#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "db.h" #include "mailsql.h" extern const char *version; #define SVR_TIMEOUT_SEND 120 #define SVR_TIMEOUT_CLI 600 static char *svr_hostname; /* Hostname of POP3 server */ /* Extract which virtual host is expected by the POP client. Return -1 if any error. */ static int vmail_getourname(SSTRING &name) { int ret = -1; struct sockaddr_in adr; unsigned int len = sizeof(adr); if (getsockname (0,(struct sockaddr*)&adr,&len) != -1){ struct hostent *ent; /* syslog (LOG_ERR,"connexion de %d %x",adr.sin_port,adr.sin_addr); */ ent = gethostbyaddr ((const char*)&adr.sin_addr ,sizeof(adr.sin_addr.s_addr),AF_INET); /* syslog (LOG_DEBUG,"connexion de %p",ent); */ if (ent != NULL){ name.setfrom (ent->h_name); name.to_lower(); ret = 0; }else{ long a = ntohl (*(long*)(&adr.sin_addr)); syslog (LOG_ERR,"Can't convert IP number %lu.%lu.%lu.%lu to name, using main domain" ,(a>>24)&0xff,(a>>16)&0xff,(a>>8)&0xff,a&0xff); } }else{ syslog (LOG_ERR,"getsockname failed (errno %m)"); } return ret; } enum SVR_STATE { SVR_DONE_STATE, SVR_SHUTDOWN_STATE, SVR_AUTH_STATE, SVR_PASS_STATE, SVR_FOLD_STATE, SVR_TRANS_STATE, }; class SESSION { public: int fd; FILE *fout; SSTRING user; int userid; SSTRING domaint; // Domain title SSTRING folder; // Current folder int domainid; int folderid; time_t createdate; time_t expiredate; time_t noticedate; bool mustnotice; // Must tell the user his account is about // to expire. size_t noticesize; // Size of the notice message int nbmsg; // How many message in the folder /*~PROTOBEG~ SESSION */ public: SESSION (int _fd); void delmsg (void); void fail (const char *msg); bool ferror (void); void fflush (void); int fld_count (void); void fld_delete (int no); SVR_STATE fld_fromsp (const char *_folder); void fld_last (void); void fld_list (int no, bool uidl); void fld_reset (void); void fld_retr (int no, int nbline); void fld_stat (void); void setcontext (void); int setdomain (const char *name); SVR_STATE svr_auth (SVR_STATE state, const char *inbuf); SVR_STATE svr_fold (SVR_STATE state, const char *inbuf); SVR_STATE svr_pass (SVR_STATE state, const char *inbuf); SVR_STATE svr_shutdown (void); SVR_STATE svr_trans (SVR_STATE state, const char *inbuf); ~SESSION (void); /*~PROTOEND~ SESSION */ }; PUBLIC SESSION::SESSION(int _fd) { fd = _fd; fout = fdopen (fd,"w"); domainid = -1; createdate = expiredate = noticedate = 0; mustnotice = false; noticesize = 0; nbmsg = 0; } PUBLIC SESSION::~SESSION() { if (fout != NULL) fclose (fout); } PUBLIC void SESSION::fflush() { ::fflush (fout); } PUBLIC bool SESSION::ferror() { return ::ferror(fout) != 0; } /* Erase all messages marked for deletion */ PUBLIC void SESSION::delmsg() { long used; glocal.used = 0; ("select messages.messageid,size from messages" " where folderid=%d and deleted='Y'" ,folderid); glocal.used += atoi(row[1]); sql_action ("delete from parts where messageid=%s",row[0]); sql_action ("delete from messages" " where folderid=%d and deleted='Y'" ,folderid); sql_action ("update accounts set used=used-%ld where userid=%d" ,glocal.used,userid); } /* Report the number of message in the current folder */ PUBLIC int SESSION::fld_count() { int count; glocal.count = 0; ("select count(*) from messages" " where folderid=%d and deleted='N'" ,folderid); glocal.count = atoi(row[0]); return glocal.count + mustnotice ? 1 : 0; } PUBLIC SVR_STATE SESSION::fld_fromsp(const char *_folder) { SESSION *session; SVR_STATE ret; glocal.session = this; glocal.ret = SVR_TRANS_STATE; ("select folderid,foldername from folders" " where userid=%d and foldername='%s'" ,userid,_folder); glocal.ret = SVR_FOLD_STATE; glocal.session->folderid = atoi(row[0]); glocal.session->folder.setfrom (row[1]); int count = glocal.session->fld_count (); fprintf (glocal.session->fout,"+OK %d messages ready for %s in %s\r\n" ,count,glocal.session->user.get(),glocal.session->folder.get()); glocal.session->nbmsg = count; return glocal.ret; } /* Retrieve a message */ PUBLIC void SESSION::fld_retr(int no, int nbline) { FILE *fout; int nbline; int no; glocal.fout = fout; glocal.nbline = nbline; glocal.no = no; if (no == nbmsg && mustnotice){ fprintf (glocal.fout,"+OK message %d (%u octets):\r\n" ,no,noticesize); ("/etc/mailsql.notice",false); fputs (line,glocal.fout); return 0; fputs (".\r\n",fout); }else{ ("select header,message,deleted from messages,parts" " where messages.messageid=parts.messageid" " and folderid=%d" " order by daterec limit %d,1" ,folderid,no-1); if (row[2][0] == 'Y'){ fprintf (glocal.fout,"-ERR message %d has been marked for deletion\r\n" ,glocal.no); }else{ fprintf (glocal.fout,"+OK message %d (%d octets):\r\n" ,glocal.no,strlen(row[0])+strlen(row[1])); fputs (row[0],glocal.fout); fputs ("\n",glocal.fout); if (glocal.nbline == -1){ fputs (row[1],glocal.fout); }else{ const char *msg = row[1]; while (*msg != '\0' && glocal.nbline > 0){ while (*msg != '\0' && *msg != '\n'){ fputc (*msg++,glocal.fout); } if (*msg == '\n'){ glocal.nbline--; fputc ('\n',glocal.fout); msg++; } } } fputs (".\r\n",glocal.fout); } fputs ("-ERR invalid message; number out of range\r\n",glocal.fout); } } PUBLIC void SESSION::fld_reset() { int count = fld_count (); fprintf (fout,"+OK %d messages ready for %s in %s\r\n" ,count,user.get(),folder.get()); } PUBLIC void SESSION::fld_stat() { long size; int count; glocal.size = 0; glocal.count = 0; ("select header,message from messages,parts" " where messages.messageid=parts.messageid" " and folderid=%d" ,folderid); glocal.size += strlen(row[0]) + strlen(row[1]); glocal.count++; nbmsg = glocal.count + mustnotice ? 1 : 0; fprintf (fout,"+OK %d %ld\r\n",nbmsg,glocal.size); } /* List the messages sizes or just one message */ PUBLIC void SESSION::fld_list(int no, bool uidl) { FILE *fout; int no; bool uidl; glocal.fout = fout; glocal.no = no; glocal.uidl = uidl; const char *fields = uidl ? "msgid" : "size"; if (no == -1){ if (uidl){ fputs ("+OK Unique-ID listing follows\r\n",fout); }else{ int count = fld_count (); fprintf (fout,"+OK %d messages; msg# and size (in octets) for undeleted messages;\r\n" ,count); } nbmsg = ("select %s from messages" " where folderid=%d and deleted = 'N' order by daterec" ,fields,folderid); if (glocal.uidl){ fprintf (glocal.fout,"%d %s\r\n",rownum+1,row[0]); }else{ fprintf (glocal.fout,"%d %d\r\n",rownum+1,atoi(row[0])); } if (mustnotice){ nbmsg++; if (glocal.uidl){ fprintf (glocal.fout,"%d notice-%lu\r\n",nbmsg,expiredate); }else{ fprintf (glocal.fout,"%d %u\r\n",nbmsg,noticesize); } } fputs (".\r\n",fout); }else{ if (no == nbmsg && mustnotice){ if (glocal.uidl){ fprintf (glocal.fout,"+OK %d notice-%lu\r\n",glocal.no,expiredate); }else{ fprintf (glocal.fout,"+OK %d %u\r\n",no,noticesize); } }else{ ("select %s from messages,parts" " where messages.messageid=parts.messageid and folderid=%d" " order by daterec limit %d,1" ,fields,folderid,no-1); if (row[2][0] == 'Y'){ fprintf (glocal.fout,"-ERR message %d has been marked for deletion\r\n" ,glocal.no); }else{ if (glocal.uidl){ fprintf (glocal.fout,"+OK %d %s\r\n",glocal.no,row[0]); }else{ fprintf (glocal.fout,"+OK %d %d\r\n",glocal.no ,strlen(row[0])+strlen(row[1])); } } fputs ("-ERR invalid message; number out of range\r\n",glocal.fout); } } } PUBLIC void SESSION::fld_delete(int no) { FILE *fout; int folderid; int no; glocal.folderid = folderid; glocal.no = no; glocal.fout = fout; if (no == nbmsg && mustnotice){ if (sql_action ("update accounts set noticedate=expiredate" " where userid=%d",userid)==-1){ fprintf (fout,"-ERR database error\r\n"); }else{ fprintf (fout,"+OK message %d marked for deletion\r\n",no); } }else{ ("select msgid,daterec from messages" " where folderid=%d" " order by daterec limit %d,1" ,folderid,no-1); NSQL_ENCODE enc; if (sql_action ("update messages set deleted='Y',daterec='%s'" " where folderid=%d and msgid='%s'" ,row[1] ,glocal.folderid ,enc.enc(row[0]))==-1){ fprintf (glocal.fout,"-ERR SQL\r\n"); }else{ fprintf (glocal.fout,"+OK message %d marked for deletion\r\n",glocal.no); } fputs ("-ERR invalid message; number out of range\r\n",glocal.fout); } } PUBLIC void SESSION::fld_last() { fputs ("+OK 0 (not done)\r\n",fout); } PUBLIC void SESSION::fail (const char *msg) { fprintf(fout,"-ERR POP3 Server Abnormal Shutdown: %s\r\n",msg); exit (0); } static void fail (const char *msg) { syslog (LOG_ERR,"Server Abnormal Shutdown: %s",msg); exit (0); } static char *svr_invalid = "-ERR Invalid command; valid commands:"; /* Prepare to shutdown POP3 server */ PUBLIC SVR_STATE SESSION::svr_shutdown() { fprintf (fout,"+OK %s POP3 SQL-Server (Version %s) shutdown.\r\n", svr_hostname,version); return SVR_DONE_STATE; } /* Server Folder State; need to open another folder */ PUBLIC SVR_STATE SESSION::svr_fold(SVR_STATE state, const char *inbuf) { if (strncasecmp(inbuf,"quit",4) == 0) return svr_shutdown(); if (strncasecmp(inbuf,"mbox",4) == 0) { inbuf = str_skip (inbuf + 4); state = fld_fromsp(inbuf); } else if (strncasecmp(inbuf,"noop",4) == 0) { fprintf (fout,"+OK\r\n"); } else { fprintf (fout,"%s MBOX, NOOP or QUIT\r\n",svr_invalid); } return state; } /* Timeout while waiting for next client command */ static void int_progerr(int) { fail("program error"); /* Exit POP3 server */ } /* Timeout while waiting for next client command */ static void svr_timeout(int ) { fail("Lost client"); /* Exit POP3 server */ } /* Timeout while waiting for next client command */ static void int_hangup(int ) { fail("Timeout"); /* Exit POP3 server */ } /**************************************************************************/ /* Initialize POP3 server */ static void initialize() { char buf[MAXHOSTNAMELEN+1]; /* Get our hostname */ gethostname(buf,MAXHOSTNAMELEN); svr_hostname = strdup (buf); if (svr_hostname == NULL) fail("Out of memory"); signal(SIGALRM, svr_timeout); // timer expiration /* Handle process signals ourself */ signal(SIGHUP, int_hangup); // socket signals signal(SIGURG, int_hangup); signal(SIGTERM, int_hangup); # ifdef SIGBUS signal(SIGBUS, int_progerr); /* fatal program errors */ # endif signal(SIGSEGV, int_progerr); signal(SIGILL, int_progerr); signal(SIGIOT, int_progerr); } /* Server Authentification State; process client USER command */ PUBLIC SVR_STATE SESSION::svr_auth( SVR_STATE state, const char *inbuf) { if (strncasecmp(inbuf,"quit",4) == 0) return svr_shutdown(); /* Expecting USER command */ if (strncasecmp(inbuf,"user",4) == 0) { inbuf = str_skip(inbuf+4); user.setfrom (inbuf); fprintf (fout,"+OK please send PASS command\r\n"); state = SVR_PASS_STATE; } else { fprintf (fout,"%s USER, QUIT\r\n",svr_invalid); } return state; } /* Setup the information for a vdomain. Return -1 if the domain does not exist */ PUBLIC int SESSION::setdomain (const char *name) { SESSION *session; glocal.session = this; int nb = ("select domainid,domain,unix_timestamp(expiredate) from domains where domain='%s' limit 1" ,name); glocal.session->domainid = atoi(row[0]); glocal.session->domaint.setfrom(row[1]); glocal.session->expiredate = atoi(row[2]); return nb>0 ? 0 : -1; } PUBLIC void SESSION::setcontext () { SESSION *session; glocal.session = this; SSTRING svr_name; if (vmail_getourname(svr_name)!=-1){ /* #Specification: virtual domain / server to domain vpop3d needs a way to identify a domain from the target of a POP request. In most organisation, one server is dedicated to email and is part of the domain which is managed. When creating a virtual email system, it is expected that one will create a virtual host like this for domain virtual.com mailserv.virtual.com (mailserv is just an example). When vpop3d gets a connection, it uses getsockname() to find out the target of the request and then do a gethostbyaddr() to get the name associated with this target. Say it is getting mailserv.virtual.com. It will check if /var/spool/vmail/mailserv.virtual.com exist. if this is the case, then it will use /etc/vmail/passwd.virtual.com. If it does not exist, it will strip the host part and try /var/spool/vmail/virtual.com. If it exists, it will use the corresponding /etc/vmail/passwd.virtual.com. If it does not exist, vpop3d will fall back to standard non-virtual operation using /var/spool/mail and /etc/passwd. */ const char *ptserv = svr_name.get(); for (int i=0; i<2; i++){ if (setdomain (ptserv)!=-1){ fprintf(fout,"+OK %s SQL POP3 Server (Version %s) ready.\r\n" ,domaint.get(),version); fflush(); break; }else{ ptserv=svr_name.strchr('.'); if (ptserv!=NULL){ ptserv++; }else{ break; } } } } } /* Server Password State; process client PASS command */ PUBLIC SVR_STATE SESSION::svr_pass( SVR_STATE state, const char *inbuf) { glocal char *pass; glocal bool found = false; glocal bool locked = false; glocal SESSION *session = this; if (strncasecmp(inbuf,"quit",4) == 0) return svr_shutdown(); /* Expecting PASS command */ if (strncasecmp(inbuf,"pass",4) != 0) { fprintf (fout,"%s PASS, QUIT\r\n",svr_invalid); return state; } /* Verify usercode/password pair */ glocal.pass = str_skip (inbuf+4); ("select userid,mayretrieve,password" ",unix_timestamp(createdate),unix_timestamp(expiredate)" ",unix_timestamp(noticedate)" " from accounts" " where domainid=%d and name='%s'" ,domainid,user.get()); if (strcmp(row[2],crypt(glocal.pass,row[2]))==0){ glocal.found = true; if (row[1][0] == 'Y'){ glocal.session->userid = atoi(row[0]); sql_action ("update accounts set lastaccess=now() where userid=%s" ,row[0]); }else{ glocal.locked = true; } glocal.session->createdate = atoi(row[3]); int expire = atoi(row[4]); if (expire != 0) glocal.session->expiredate = expire; glocal.session->noticedate = atoi(row[5]); } time_t now = time(NULL); if (!glocal.found){ fprintf (fout,"-ERR invalid usercode or password, please try again\r\n"); return SVR_AUTH_STATE; }else if (glocal.locked){ fprintf (fout,"-ERR mailbox is locked, contact administrator\r\n"); return SVR_AUTH_STATE; }else if (createdate > 0 && now < createdate){ fprintf (fout,"-ERR mailbox is not available now, contact administrator\r\n"); return SVR_AUTH_STATE; }else if (expiredate > 0 && now > expiredate){ struct stat st; bool fexist = stat ("/etc/mailsql.notice",&st) != -1; if (!fexist || now > expiredate + 15*24*60*60){ fprintf (fout,"-ERR account has expired, contact administrator\r\n"); return SVR_AUTH_STATE; }else if (fexist && noticedate == 0){ mustnotice = true; noticesize = st.st_size; } } syslog (LOG_NOTICE,"Auth ok:domainid='%d',name='%s'",domainid,user.get()); return fld_fromsp("inbox"); } /* Server Transaction State; process client mailbox command */ PUBLIC SVR_STATE SESSION::svr_trans( SVR_STATE state, const char *inbuf) { int msgnum; if (strncasecmp(inbuf,"quit",4) == 0){ state = svr_shutdown(); }else if (strncasecmp(inbuf,"dele",4) == 0) { inbuf = str_skip(inbuf+4); if (*inbuf == '\0'){ fprintf (fout,"-ERR message number required (e.g. DELE 1)\r\n"); }else{ fld_delete(atoi(inbuf)); } } else if (strncasecmp(inbuf,"host",4) == 0) { fprintf (fout,"-ERR host command not supported\r\n"); } else if (strncasecmp(inbuf,"last",4) == 0) { fld_last(); } else if (strncasecmp(inbuf,"list",4) == 0) { inbuf = str_skip(inbuf+4); if (*inbuf == '\0'){ fld_list(-1,false); }else{ fld_list(atoi(inbuf),false); } } else if (strncasecmp(inbuf,"uidl",4) == 0) { inbuf = str_skip(inbuf+4); if (*inbuf == '\0'){ fld_list(-1,true); }else{ fld_list(atoi(inbuf),true); } } else if (strncasecmp(inbuf,"mbox",4) == 0) { inbuf = str_skip (inbuf + 4); state = fld_fromsp(inbuf); } else if (strncasecmp(inbuf,"noop",4) == 0) { fprintf (fout,"+OK\r\n"); } else if (strncasecmp(inbuf,"retr",4) == 0) { inbuf = str_skip(inbuf+4); if (*inbuf == '\0') { fprintf (fout,"-ERR message number required (e.g. RETR 1)\r\n"); } else{ fld_retr(atoi(inbuf),-1); } } else if (strncasecmp(inbuf,"rset",4) == 0) { fld_reset(); } else if (strncasecmp(inbuf,"stat",4) == 0) { fld_stat(); } else if (strncasecmp(inbuf,"top",3) == 0) { inbuf = str_skip(inbuf+3); if (*inbuf == '\0') { fprintf (fout,"-ERR message number and line count required (e.g. TOP 1 7)\r\n"); } else { msgnum = atoi(inbuf); inbuf = str_skipdig(inbuf); inbuf = str_skip(inbuf); if (*inbuf == '\0'){ fprintf (fout,"-ERR line count required (e.g. TOP 1 7)\r\n"); }else{ fld_retr(msgnum,atoi(inbuf)); } } } else { fprintf (fout ,"%s DELE, HOST, LAST, LIST, MBOX, NOOP, RETR, RSET, STAT, TOP or QUIT\r\n", svr_invalid); } return state; } /* Read a command from stdin and strip the blanks at the end */ static int vpop3d_readcmd (SSTRING &line) { int ret = -1; char buf[1000]; if (fgets(buf,sizeof(buf)-1,stdin)!=NULL){ strip_end (buf); line.setfrom (buf); ret = 0; syslog(LOG_DEBUG,"POP client command: %s",buf); } return ret; } int main(int argc, char *argv[]) { tlmp_initmod(); initialize(); openlog ("vpop3d_sql",LOG_PID,LOG_DAEMON); vutil_opendb(); SESSION session (0); session.setcontext (); if (session.domainid == -1){ if (argc >= 2){ // We jump to another (standard) POP3D daemon execvp (argv[1],argv+1); }else{ syslog (LOG_DEBUG,"Unknown SQL vdomain, using IPless"); fprintf(stdout,"+OK (IP less mode) SQL POP3 Server (Version %s) ready.\r\n" ,version); } } fflush(stdout); SVR_STATE svr_state = SVR_AUTH_STATE; while (svr_state != SVR_DONE_STATE){ // Wait for command from client alarm(SVR_TIMEOUT_CLI); SSTRING line; if (vpop3d_readcmd (line) == -1) break; alarm(0); const char *ptline = line.get(); // Take action on client command switch(svr_state) { case SVR_AUTH_STATE: // Expecting USER command { SSTRING tmp; const char *pt = strchr(ptline,'/'); if (pt == NULL) pt = strchr(ptline,'@'); if (pt != NULL){ tmp.setfrom (ptline,(int)(pt-ptline)); ptline = tmp.get(); pt++; if (session.setdomain (pt)==-1){ fprintf (session.fout,"-ERR Invalid domain\n"); break; } } svr_state = session.svr_auth(svr_state,ptline); } break; case SVR_PASS_STATE: // Expecting PASS command svr_state = session.svr_pass(svr_state,ptline); break; case SVR_TRANS_STATE: // Expecting mailbox command svr_state = session.svr_trans(svr_state,ptline); break; case SVR_FOLD_STATE: /* Need to open another mailbox */ svr_state = session.svr_fold(svr_state,ptline); break; default: session.fail("Confused"); /* Wont return */ break; } /* Send out response to client */ alarm(SVR_TIMEOUT_SEND); session.fflush(); alarm(0); if (session.ferror()) break; } session.delmsg(); return 0; }