joemc91 n00b
Joined: 04 Feb 2004 Posts: 38
|
Posted: Tue Jun 22, 2004 3:09 am Post subject: |
|
|
I couldn't find any mailing list managers with a database backend that I like, so I rolled my own simple one. Not very powerful (doesn't have to be), but it already supports threaded archiving, user lists, multiple domains, and joining requests by email. I know it's a small feature list, but not too bad IMO for a short PHP script.
The database has 7 tables: admins (administrative users, will eventually be used for managing purposes), archive (stores all archived messages), domains (all virtual domains hosted by the list), local_aliases (the two aliases that the list uses "sendmail.php" and "request.php"), mailing_list (the mailing lists matching addresses to the sendmail and request scripts), master_list (list of all addresses hosted), subscriptions (email addresses subscribing to mailing lists. Used to authorize emails to pass through the list.
Here is the database script, contains only classes used to access the database (no commenting yet, bad habit):
Code: | database.php
<?php
/*The purpose behind the database class is to provide easily changed functions for all databases on the website
this allows for an easy change over in the future to another database such as mysql*/
# This file only provides the functionality to perform actions on the mailing list database.
class Database
{
var $dataResource;
function Database() #initialize the database connection and all variables
{
$this->dataResource = mysql_connect("localhost",user,password);
}
function closeDatabase() #close the database connection. The reason for calling this function instead of the mysql function directly is to be able to change the database type with minimal effort
{
mysql_close($this->dataResource);
}
function queryDatabase($query) #query the database, parameter should be in the form of and SQL query, such as SELECT * FROM tablename
{
return mysql_db_query("postfix",$query,$this->dataResource);
}
function fetchRow($result) #objects are returned because they are easier to deal with than arrays
{
return mysql_fetch_object($result);
}
function freeResult($result) #frees up a result resource identifier, if you need to
{
mysql_free_result($result);
}
function numRows($result) #get number of rows in a result
{
return mysql_num_rows($result);
}
function valueExists($query)
{
$result = $this->queryDatabase($query);
if ($this->numRows($result) == 0)
{ return false; }
else
{ return true; }
}
function max($table,$field) #returns the max value of a field name from a table
{
$result = $this->queryDatabase("SELECT MAX($field) FROM $table");
$maxarray = mysql_fetch_row($result);
return $maxarray[0];
}
function toMySQLString($text)
{
return mysql_escape_string($text);
}
} #end class database
class MailingList extends Database
{
function MailingList()
{
$this->Database();
}
function acceptMailer($email,$listname)
{
return $this->valueExists("SELECT address FROM subscriptions WHERE address LIKE '$email' AND subscription LIKE '$listname'");
}
function subscribe($email,$listname)
{
if (!$this->acceptMailer($email,$listname) && $this->valueExists("SELECT address FROM master_list WHERE address LIKE '$listname'"))
{ $this->queryDatabase("INSERT INTO subscriptions (address, subscription) VALUES ('$email', '$listname')"); }
}
function unsubscribe($email,$listname)
{
if ($this->acceptMailer($email,$listname))
{ $this->queryDatabase("DELETE FROM subscriptions WHERE address LIKE '$email' AND subscription LIKE '$listname'"); }
}
function update($oldemail,$newemail,$listname)
{
if ($this->acceptMailer($oldemail,$listname))
{ $this->queryDatabase("UPDATE subscriptions SET address = '$newemail' WHERE address LIKE '$oldemail' AND subscription LIKE '$listname'"); }
else
{ $this->subscribe($newemail,$listname); }
}
function getEmailsInList($listname)
{
$result = $this->queryDatabase("SELECT address FROM subscriptions WHERE subscription LIKE '$listname'");
$emails = array();
while ($row = $this->fetchRow($result))
{
$emails[] = $row->address;
}
return $emails;
}
}
class Archive extends Database
{
function Archive()
{
$this->Database();
}
function archiveMessage($listname,$from,$subject,$message_id,$parent_id,$body)
{
$body = $this->toMySQLString($body);
$subject = $this->toMySQLString($subject);
if ($parent_id == "") { $parent_id = "top"; }
$query = "INSERT INTO archive (mailing_list, from_address, subject, message_id, parent_id, body) VALUES ('$listname','$from','$subject','$message_id','$parent_id','$body')";
$this->queryDatabase($query);
return mysql_error();
}
}
?> |
This is the sendmail script. All mail passing through the lists is processed by this (again no commenting, bad habits):
Code: | sendmail.php
#!/usr/bin/php
<?
function getTextBodyOnly($part)
{
if (strcasecmp($part->ctype_primary,"multipart") == 0)
{ return getTextBodyOnly($part->parts[0]); }
else { return $part->body; }
}
function encodePart($email)
{
$content['content_type'] = $email->ctype_primary . "/" . $email->ctype_secondary;
$mainpart = new Mail_mimePart('',$content);
$subparts = array();
foreach ($email->parts as $part)
{
$params = array();
if (strcasecmp($part->ctype_primary,"multipart") == 0)
{
$newpart = encodePart($part);
$params['content_type'] = $newpart['headers']['Content-Type'];
$body = $newpart['body'];
}
else
{
$params['content_type'] = $part->ctype_primary . "/" . $part->ctype_secondary;
$params['encoding'] = $part->headers['content-transfer-encoding'];
if ($part->disposition != "")
{
$params['disposition'] = $part->disposition;
$params['dfilename'] = join("",$part->d_parameters);
}
$body = $part->body;
}
$subparts[] = $mainpart->addSubPart($body,$params);
}
return $mainpart->encode();
}
include "Mail/mimeDecode.php";
include "Mail/smtp.php";
include "Mail/mimePart.php";
include "database.php";
$mailing_list = new MailingList();
$emailarray = file("php://stdin");
$email = join("",$emailarray);
$decoder = new Mail_mimeDecode($email);
$emailparts = $decoder->decode('include_bodies');
$from = $emailparts->headers['from'];
preg_match("<[\d\w_-]+@[\d\w-_]+\.[\w]+>",$from,$matches);
$from = $matches[0];
if ($mailing_list->acceptMailer($from, $emailparts->headers['x-original-to']))
{
$headers = array();
$headers['To'] = $emailparts->headers['to'];
$headers['From'] = $emailparts->headers['from'];
$headers['Reply-To'] = $emailparts->headers['x-original-to'];
$headers['Subject'] = $emailparts->headers['subject'];
$headers['Message-ID'] = $emailparts->headers['message-id'];
$headers['Content-Type'] = $emailparts->ctype_primary . "/" . $emailparts->ctype_secondary;
$archive = new Archive();
$textonlybody = getTextBodyOnly($emailparts);
$archive->archiveMessage($headers['Reply-To'],$headers['From'],
$headers['Subject'],$headers['Message-ID'],$emailparts->headers['in-reply-to'],$textonlybody);
if (strcasecmp($emailparts->ctype_primary,"text") == 0)
{
$body = $emailparts->body;
}
if (strcasecmp($emailparts->ctype_primary,"multipart") == 0)
{
$newemail = encodePart($emailparts);
$body = $newemail[body];
$newemail['headers']['Mime-Version'] = '1.0';
$headers = array_merge($headers,$newemail[headers]);
}
$emailsinlist = $mailing_list->getEmailsInList($emailparts->headers['x-original-to']);
$recipients = join(", ",$emailsinlist);
$mail_object =& Mail::factory('smtp');
$mail_object->send($recipients,$headers,$body);
}
else
{
$request = str_replace("@","-request@",$emailparts->headers['x-original-to']);
mail($emailparts->headers['from'],"Permission denied to " . $emailparts->headers['x-original-to'],
"Your email address does not have permission to send mail to this mailing list, you must be a member to send mail to this address.\n\n" .
"To become a member, send a message to $request with a subject of \"subscribe\"\n\n" .
"This message automatically generated by PHP Mailing List\n\n" .
"From: " . $emailparts->headers['x-original-to']);
}
?> |
Here is the request script, any mail sent to a list-REQUEST (emphasis mine) is processed by this (no comments, see above):
Code: | request.php
#!/usr/bin/php
<?
include "Mail/mimeDecode.php";
include "database.php";
$emailarray = file("php://stdin");
$mailing_list = new MailingList();
$email = join("",$emailarray);
$decoder = new Mail_mimeDecode($email);
$emailparts = $decoder->decode('include_bodies');
$from = $emailparts->headers['from'];
preg_match("<[\d\w_-]+@[\d\w-_]+\.[\w]+>",$from,$matches);
$from = $matches[0];
$subject = $emailparts->headers['subject'];
$list = $emailparts->headers['x-original-to'];
$list = str_replace("-request@","@",$list);
if (strcasecmp($subject,"subscribe") == 0)
{
$mailing_list->subscribe($from,$list);
mail ($from,"You have succesfully joined the $list mailing list","This is an automatically generated message","From: " . str_replace("@","-request@",$list));
} else
if (strcasecmp($subject,"unsubscribe") == 0)
{
$mailing_list->unsubscribe($from,$list);
mail ($from,"You have succesfully left the $list mailing list","This is an automatically generated message","From: " . str_replace("@","-request@",$list));
}
else
{
mail ($from,"Unknown request","The command requested is unknown\n\nTo perform a command, send an email to "
. str_replace("@","-request@",$list)
. " with ONLY the request in the subject (not case sensitive)\n\n"
. "The supported commands are:\n"
. " subscribe\n unsubscribe\n\n"
. "Thank you for using PHP Mail List",
"From: " . str_replace("@","-request@",$list));
}
?> |
In my main.cf file for postfix, I have added the following:
Code: | virtual_mailbox_domains = mysql:/etc/mail/mysql_domains.cf
virtual_alias_maps = mysql:/etc/mail/mysql_virtual.cf
alias_maps = mysql:/etc/mail/mysql_aliases.cf |
I use virtual_mailbox_domains because I will eventually host mailboxes for the domains on this machine. The .cf files actually tell postfix which tables and fields to query for aliases and domains to accept email for.
I realize that this code looks hacked together (looks aren't decieving in this case), but it works pretty well so far. I'll eventually write webpages to administer the database without having to muck about in the database and keep the data consistent. Tell me what you think! |
|