Interfacing QM with Linux Email
5/23/06 - Doug Dumitru
Background:
I had a client that maintained a mid-size contact database on a QM server. The database included a field with email addresses. The database was being maintained with a QM/Coyote web-based application.
The client wanted an easy way to send mass emails to all of the email addresses in the database. As is typical, the client tools for sending emails consited of an ISP supplied mail application with no mass mailing capabilities.
The Solution:
I setup an inbound mailbox that would deliver mail to QM instead of to a Unix mailbox. QM would then strip extra headers off of the mail message and resend the message to every record in the database with a valid email address.
The Server:
This application is hosted on a public-facing Linux server running Fedora Core 5. It is actually a "Xen" virtual server, but as far as applications running inside of the server, it is a dedicated box.
Sendmail Configuration:
The only MTA (Mail Transport Agent) software on the server is sendmail, definately not the most plesant to configure. In this case, the setup was so simple, that sendmail was fine.
Step 1: Setup a domain name to point to the server.
Mail requires DNS to run. If you have a simple domain name, or a subdomain, you can just point the domain to the IP address of the server. If you want to setup an 'MX' record you can, but this is not required. We gave the customer a throwaway record under one of our "infrastructure" names:
customername.easyco.net IN A 1.2.3.4
Step 2: Setup sendmail to recognize the customer's domain name as a local domain.
This involves adding 'customername.easyco.net' to /etc/mail/local-host-names.
Step 3: Setting up sendmail for inbound mail.
The default configuration for sendmail only listens on 127.0.0.1 (the loopback address). In order to accept mail from the outside world, you need to change this to 0.0.0.0. This is done in /etc/mail/sendmail.mc line:
DAEMON_OPTIONS(`Port=smtp,Addr=0.0.0.0,Name=MTA')dnl
Step 4: Recompile the sendmail configuration file.
With Fedora Core this is easy. Just cd to /etc/mail and run 'make'. After you rebuild the sendmail configuration file, you should tell sendmail to reload it's configuration with '/etc/rc.d/init.d/sendmail reload'.
Step 5: Open the firewall
In order for mail to move, you need to open port 25 both inbound and outbound.
Step 6: Setup an 'alias' for the recieved email address.
The address that will relay is a "special" address that does not have a user associated with it. In this case, it is also "private", so we picked an address that would not get "guessed" by the spammers. You then enter this address at the end of /etc/aliases
custname-db-873466542: "|qm all"
This setup allows you to send email to custname-db-873466542@customername.easyco.net and the mail message itself will be delivered to the 'qm.sh' script.
After you add an alias, remember to run the 'newaliases' command to rebuild the aliases database.
Step 7: Setup the 'qm.sh' script
Sendmail delivers email thru scripts in /etc/smrsh. In this case, a 5-line bash script was used to invoke qm.
#!/bin/bash MSG=/tmp/$$.txt cat - > $MSG cd /usr/qmaccount /usr/qmsys/bin/qm -quiet EMAIL-FWD $1 $MSG >> /var/log/qm.sh rm $MSG
QM Programming
So now we have sendmail delivering email messages to QM. It does this by running the QM program EMAIL-FWD passing two parameters on the command line. The first parameter is a command that will tell EMAIL-FWD what to do. Perhaps there are several lists attached to several email addresses, so this is a way to re-use the EMAIL-FWD program for several purposes. The second parameter is the Linux path to a temporary file that contains the email message itself.
The EMAIL-FWD program.
This is a pretty simple QM/Basic program:
rem EMAIL-FWD $catalog local cmd = field(sentence(),' ',2) MsgId = field(sentence(),' ',3) o = object('Email.cls') err = o->ReadMsg(MsgId) if err <> '' then abort err err = o->StripHdrs if err <> '' then abort err cmds = '' cmds<1> = 'all' cmds<2> = 'test' t = listindex(cmds,@am,downcase(cmd)) + 1 on t goto cmd.undef, cmd.all, cmd.test abort cmd.undef: abort 'EMAIL-FWD undefined command' cmd.all: gosub GetAll(name.lst,email.lst) for i = 1 to dcount(name.lst,@am) err = o->SendMsg(name.lst<i>,email.lst<i>) next i gosub SendSummary('Admin person 1','addr1@example.com',name.lst,email.lst) gosub SendSummary('Admin person 2','addr2@example.com',name.lst,email.lst) stop cmd.test: gosub GetAll(name.lst,email.lst) err = o->SendMsg('Admin person 1','addr1@example.com') err = o->SendMsg('Admin person 2','addr2@example.com') gosub SendSummary('Admin person 1','addr1@example.com',name.lst,email.lst) gosub SendSummary('Admin person 2','addr2@example.com',name.lst,email.lst) stop LOCAL SUBROUTINE GetAll(name.lst,email.lst) PRIVATE db.fd PRIVATE name,emails,id,d open 'DATABASE-FILE' to db.fd else abort 'Cannot open DATABASE-FILE' name.lst = '' email.lst = '' execute 'SSELECT DATABASE-FILE BY SNAME' capturing dummy loop readnext id else exit read d from db.fd , id else continue name = trim(d<1>:' ':d<2>) if name = '' then continue emails = trim(d<15>) if emails = '' then continue emails = change(emails,',',@am) for i = 1 to dcount(emails,@am) email = emails<i> if count(email,'@') <> 1 then continue name.lst<-1> = name email.lst<-1> = email next i repeat return LOCAL SUBROUTINE SendSummary(to.name,to.email,name.lst,email.lst) PRIVATE s.msg,i,err s.msg = 'Subject: Message Summary' s.msg<-1> = 'From: "DATABASE-FILE database server" <' : o->FromAddr : '>' s.msg<-1> = 'To: "Me" <addr1@example.com>' s.msg<-1> = '' s.msg<-1> = 'The folllowing addresses were used:' s.msg<-1> = '' for i = 1 to dcount(name.lst,@am) s.msg<-1> = ' ' : name.lst<i> 'l#30' : ' ' : email.lst<i> next i o->SetMsg(s.msg) err = o->SendMsg(to.name,to.email) return end
This program uses a class named 'Email.cls':
CLASS Email.cls $catalog global PRIVATE Msg PRIVATE FromAddr PRIVATE cr,tab PUBLIC SUBROUTINE create.object cr = char(13) tab = char(9) end GET ReadMsg(path) err = '' openseq path to src.fd else err<-1>= 'Email.cls: ReadMsg: Cannot open message source file ' : path return(err) end Msg = '' loop readseq l from src.fd else exit l = change(l,cr,'') Msg<-1> = l repeat return(err) end SET SetMsg(n.msg) Msg = n.msg end GET WriteMsg(path) err = '' openseq path OVERWRITE to dst.fd else create dst.fd else err<-1> = 'Email.cls: WriteMsg: Cannot open/create outbound message file ' : path return(err) end end i1 = dcount(Msg,@am) for i = 1 to i1 writeseq Msg<i> to dst.fd else err<-1> = 'Email.cls: WriteMsg: Error writing outbound message file' return(err) end next i return(err) end GET StripHdrs err = '' if Msg = '' then err<-1> = 'Email.cls: StripHdrs: No message in queue.' return(err) end l.msg = Msg Msg = '' FromAddr = '' CurState = '' i1 = dcount(l.msg,@am) i = 1 loop if i > i1 then exit l = l.msg<i> begin case case CurState = '' if l = '' then Msg<-1>= '' CurState = 'body' i += 1 continue end if downcase(field(trim(l),' ',1)) = 'from' then if FromAddr <> '' then err<-1>= 'Email.cls: StripHdrs: Duplicate From line.' return(err) end FromAddr = field(trim(l),' ',2) i += 1 continue end w = downcase(trim(field(l,':',1))) begin case case w = 'received' CurState = 'skiphdr' case 1 Msg<-1> = l end case i += 1 continue case CurState = 'hdr' Msg<-1> = l i += 1 continue case CurState = 'skiphdr' begin case case l[1,1] = ' ' or l[1,1] = tab i += 1 case 1 CurState = '' end case continue case CurState = 'body' Msg<-1> = l i += 1 continue end case repeat return(err) end GET ReplToHdr(val) err = '' if Msg = '' then err<-1> = 'Email.cls: ReplToHdr: Message is not loaded.' return(err) end l.msg = Msg Msg = '' CurState = '' i1 = dcount(l.msg,@am) i = 1 loop if i > i1 then exit l = l.msg<-1> begin case case CurState = '' if l = '' then Msg<-1> = '' CurState = 'body' i += 1 continue end w = downcase(trim(field(l,':',1))) begin case case w = 'to' CurState = 'skiphdr' Msg<-1>= 'To: ' : val case 1 Msg<-1> = l end case i += 1 continue case CurState = 'hdr' Msg<-1> = l i += 1 continue case CurState = 'skiphdr' begin case case l[1,1] = ' ' or l[1,1] = tab i += 1 case 1 CurState = '' end case continue case CurState = 'body' Msg<-1> = l i += 1 continue end case repeat return(err) end GET SendMsg(ToName,ToAddr) err = '' if Msg = '' then err<-1> = 'Email.cls: SendMsg: No message is loaded.' return(err) end if ToName <> '' then s = '"' : ToName : '" <' : ToAddr : '>' end else s = ToAddr end err<-1> = me->ReplToHdr(s) if err <> '' then return(err) t.file = '/tmp/' : @userno : '_qm.txt' err<-1> = me->WriteMsg(t.file) if err <> '' then return(err) cmd = 'cat ' : t.file : '| /usr/sbin/sendmail -f' : me->FromAddr : ' ' : ToAddr * display 'cmd = ' : cmd os.execute cmd os.execute 'rm ' : t.file return(err) end GET FromAddr return(FromAddr) end GET Msg return(Msg) end end
Summary:
So there you have it. The setup needed to get sendmail to deliver mail to QM. Some QM to mangle the message headers, and then QM code to spit the message back out to hundreds of recipients. It is not optimized to be fast, but then again, I am not a spammer sending out millions of email messages. It should load on just about any current Linux distribution with sendmail and either commercial or GPL QM 2.4-4 or later. Mostly, it should help you get an idea of how sendmail and QM can cooperate sending and receiving email messages.
Because windows works very differently, moving this code to windows would be difficult.