/* FARGOS Development, LLC Sample Programs Copyright (C) 2002 FARGOS Development, LLC. All rights reserved. mailto:support@fargos.net for assistance. NOTE: in the future, enhanced versions of these classes may be added to the FARGOS/VISTA Object Management Environment core within the Standard namespace. Developers can avoid any potential conflict if they specify an explicit namespace when creating instances of the sample classes. */ %include global SMTP { const string SMTP_SERVER_ID = "FARGOS/VISTA SMTP Server 1.0"; const int MAX_IDLE_TIME = 300; }; class Local . SMTPserver { assoc configParams; oid listenObj; int connectionsAccepted; } inherits from Object; SMTPserver:create(string listenAtAddr, string hostName) { string smtpHostName, listenAddress; if (length(hostName) > 0) { smtpHostName = hostName; } else { smtpHostName = getSystemInfoAttribute("hostName"); } configParams["hostName"] = smtpHostName; if (typeOf(listenAtAddr) == string) { listenAddress = listenAtAddr; } else { listenAddress = "tcp:0.0.0.0:25,l"; } assoc connACL = makePermitEveryoneACL(); assoc acl = makeDefaultACL(); listenObj = send "createObject"("AcceptConnection", acl, listenAddress, thisObject, connACL) to ObjectCreator; send "addNotifyOnShutdown"(thisObject) to "ShutdownService"; logOutput(LOG_INFO, "SMTPserver operating on ", listenAddress, "\n"); } SMTPserver:delete() { if (listenObj != nil) { send "deleteYourself" to listenObj; } } SMTPserver:systemShutdown() { /*! Because it provides a long-running service, class=SMTPserver objects register themselves with the class=ShutdownService, which sends a method=systemShutdown notification when a graceful shutdown is requested. !*/ // TO DO: consider deletion without shutdown... send "deleteYourself" to thisObject; } SMTPserver:connectionAccepted(oid newSocket) { /*! When new HTTP connections are received, the method=connectionAccepted method creates a class=HTTPfastReceive object to handle the I/O and processing of any requests. !*/ // becomePseudoUser(); connectionsAccepted += 1; assoc acl = makeDefaultACL(); send "createObject"("SMTPclientConnection", acl, newSocket, configParams) to ObjectCreator from nil; // don't bother with response } class Local . SMTPclientConnection { enum ParseStates { HELLO, MAIL, DATA, DONE }; oid connObj; oid readBfr; oid mailboxService; string peerAddress; string claimedPeerName; int lastRequestTime; assoc serverConfig; oid timerObj; int currentParseState; set recipients; set recipientAddrList; set senders; set mailBody; int sessionClosed; oid shutdownOID; } inherits from Object; SMTPclientConnection:create(oid newSocket, assoc configParams) { connObj = newSocket; serverConfig = configParams; peerAddress = send "getPeerAddress" to connObj; logOutput(LOG_INFO, "New SMTP connection from ", peerAddress, "\n"); mailboxService = lookupLocalService("MailboxDirectory"); if (mailboxService == nil) { send "writeBytes"("421 No MailboxDirectory\r\n") to connObj from nil; send "deleteYourself" to thisObject; exit; } lastRequestTime = getLocalRelativeTime(); assoc currentTime = convertLocalRelativeTimeToAbsolute(lastRequestTime, 0); string date = rfc1123Date(currentTime); string initialLine = makeAsString("220 ", serverConfig["hostName"], " ", SMTP_SERVER_ID, "; local time is ", date, "\r\n"); int rc = send "writeBytes"(initialLine) to connObj; logOutput(LOG_DEBUG, "wrote len=", rc, "\n"); if (rc <= 0) { logOutput(LOG_ERROR, "Could not announce to client: ", initialLine, "\n"); send "deleteYourself" to connObj; send "deleteYourself" to thisObject; exit; } currentParseState = HELLO; assoc acl = makeDefaultACL(); readBfr = send "createObject"("ReadBuffer", acl, connObj) to ObjectCreator; // NOTE: end of line delimeter must be set to CR/LF for correctness. // RFC 2821, section 4.1.1.4 requires that a single LF MUST NOT // be acccepted, even if it would seem to tolerate incorrect // implementations. This is one case where being tolerant in // what one accepts is explicitly disallowed. send "setDelimeter"("\r\n") to readBfr; send "selectForRead" to readBfr; timerObj = send "createObject"("TimerEvent", acl, thisObject, MAX_IDLE_TIME) to ObjectCreator; // TO DO: only permit systemShutdown method shutdownOID = thisObject; send "addNotifyOnShutdown"(shutdownOID) to "ShutdownService"; } SMTPclientConnection:delete() { if (readBfr != nil) { send "deleteYourself" to readBfr; } if (shutdownOID != nil) { send "removeNotifyOnShutdown"(shutdownOID) to "ShutdownService"; } } SMTPclientConnection:timerExpired(oid timerObj) { if (sessionClosed == 1) { logOutput(LOG_INFO, "Session was closed, time to delete\n"); send "deleteYourself" to thisObject; exit; } int t = getLocalRelativeTime(); int delta = t - lastRequestTime; if (delta > MAX_IDLE_TIME) { logOutput(LOG_ERROR, "SMTP client connection with ", peerAddress, " timed out\n"); send "deleteYourself" to thisObject; exit; } assoc acl = makeDefaultACL(); timerObj = send "createObject"("TimerEvent", acl, thisObject, MAX_IDLE_TIME, t) to ObjectCreator; } SMTPclientConnection:canRead(oid bfr) { lastRequestTime = getLocalRelativeTime(); any line = send "readLine" to readBfr; logOutput(LOG_DEBUG, "SMTP client line=", line, "\n"); if (line == nil) { // EOF logOutput(LOG_ERROR, "Unexpected EOF from SMTP client ", peerAddress, "\n"); send "deleteYourself" to readBfr; readBfr = nil; sessionClosed = 1; exit; } if (currentParseState == DATA) { logOutput(LOG_DEBUG, "accepting message body\n"); if (line != ".") { if (length(line) > 0) { if (midchar(line, 0) != '.') mailBody += line; else { // delete initial period mailBody += midstr(line, 1, length(line) - 1); } } mailBody += "\r\n"; } else { // process mail message... call "_processMail"(); } } else { string cmd; any tokens = tokenizeString(line, " \t\r\n", 0); logOutput(LOG_INFO, "tokenized line=", tokens); if (elementCount(tokens) > 0) { cmd = convertCase(tokens[0], 0); } else { cmd = ""; // bogus command } if (cmd == "HELO") { call "_helo"(tokens); } else if (cmd == "EHLO") { call "_ehlo"(tokens); } else if (cmd == "MAIL") { call "_mail"(tokens); } else if (cmd == "RCPT") { call "_rcpt"(tokens); } else if (cmd == "DATA") { if (elementCount(tokens) != 1) { send "writeBytes"("501 No argument expected\r\n") to connObj from nil; } else { send "writeBytes"("354 Send mail body; end with .\r\n") to connObj from nil; } call "_addTraceLine"(); currentParseState = DATA; } else if (cmd == "QUIT") { if (elementCount(tokens) != 1) { line = "501 No argument expected\r\n"; } else { line = makeAsString("221 ", serverConfig["hostName"], " is now closing transmission channel\r\n"); } send "writeBytes"(line) to connObj from nil; send "deleteYourself" to readBfr; readBfr = nil; sessionClosed = 1; exit; } else if (cmd == "RSET") { call "_reset"(); if (elementCount(tokens) != 1) { send "writeBytes"("501 No argument expected\r\n") to connObj from nil; } else { send "writeBytes"("250 OK Reset was done\r\n") to connObj from nil; } } else if (cmd == "VRFY") { call "_vrfy"(tokens); } else if (cmd == "EXPN") { call "_expn"(tokens); } else if (cmd == "HELP") { call "_help"(tokens); } else if (cmd == "NOOP") { send "writeBytes"("250 OK Did nothing\r\n") to connObj from nil; } else { send "writeBytes"("500 Command not implemented\r\n") to connObj from nil; } } send "selectForRead" to readBfr; } SMTPclientConnection:_reset() { mailBody = emptySet; recipients = emptySet; recipientAddrList = emptySet; senders = emptySet; currentParseState = HELLO; return (0); } SMTPclientConnection:_addTraceLine() { int requestTime = getLocalRelativeTime(); assoc currentTime = convertLocalRelativeTimeToAbsolute(lastRequestTime, 0); string traceLine = makeAsString("Received: from ", claimedPeerName, " ([", peerAddress, "])", "\r\n", " ", "by ", serverConfig["hostName"], " (", SMTP_SERVER_ID, ")", // with SMTP id NNNNNNN "\r\n", " ", "via TCP", " ", "with SMTP", "\r\n", " ", "for ", recipientAddrList, "; ", rfc1123Date(currentTime), "\r\n"); mailBody += traceLine; return (0); } SMTPclientConnection:_helo(array tokens) { call "_reset"(); if (typeOf(tokens[1]) == string) claimedPeerName = tokens[1]; else claimedPeerName = "didNotSay"; string response = makeAsString("250 ", serverConfig["hostName"], " Hello ", peerAddress, "\r\n"); send "writeBytes"(response) to connObj from nil; return (0); } SMTPclientConnection:_ehlo(array tokens) { call "_reset"(); if (typeOf(tokens[1]) == string) claimedPeerName = tokens[1]; else claimedPeerName = "didNotSay"; // Announce support for RFC 1652 - 8Bit-MIMEtransport string response = makeAsString("250-", serverConfig["hostName"], " Hello ", peerAddress, "\r\n", "250 8BITMIME\r\n"); send "writeBytes"(response) to connObj from nil; return (0); } SMTPclientConnection:_help(array tokens) { set lines; lines += "214-FARGOS/VISTA SMTP Server\r\n"; lines += "214-Commands: HELO ELHO MAIL RCPT DATA QUIT\r\n"; lines += "214- VRFY EXPN\r\n"; lines += "214- RSET NOOP HELP\r\n"; lines += "214 End of HELP output\r\n"; string l = makeAsString(lines); send "writeBytes"(l) to connObj from nil; return (0); } SMTPclientConnection:_mail(array tokens) { string user; if (currentParseState != HELLO) { send "writeBytes"("503 Transaction already in progress\r\n") to connObj from nil; return (0); } if (elementCount(tokens) < 2) { send "writeBytes"("501 FROM: argument required\r\n") to connObj from nil; return (0); } if (tokens[1] == "FROM:") { user = tokens[2]; } else { string fromCmd = midstr(tokens[1], 0, 5); fromCmd = convertCase(fromCmd, 0); if (fromCmd != "FROM:") { send "writeBytes"("501 Mailbox Syntax error -- no FROM:\r\n") to connObj from nil; return (0); } user = midstr(tokens[1], 5, length(tokens[1]) - 5); } senders += user; send "writeBytes"("250 OK\r\n") to connObj from nil; currentParseState = MAIL; return (0); } SMTPclientConnection:_rcpt(array tokens) { string response, user; if (currentParseState != MAIL) { send "writeBytes"("503 Need to issue MAIL before RCPT\r\n") to connObj from nil; return (0); } if (elementCount(tokens) < 2) { send "writeBytes"("501 TO: argument required\r\n") to connObj from nil; return (0); } if (tokens[1] == "TO:") { user = tokens[2]; } else { string toCmd = midstr(tokens[1], 0, 3); toCmd = convertCase(toCmd, 0); if (toCmd != "TO:") { send "writeBytes"("553 Mailbox Syntax error -- no TO:\r\n") to connObj from nil; return (0); } user = midstr(tokens[1], 3, length(tokens[1]) - 3); } oid mailbox = send "lookupMailbox"(user) to mailboxService; if (mailbox == nil) { response = makeAsString("550 ", user, " user unknown\r\n"); send "writeBytes"(response) to connObj from nil; return (0); } recipients -= mailbox; // prune out any duplicates -- deliver once recipients += mailbox; assoc info = send "getMailboxInfo" to mailbox; if (recipientAddrList != emptySet) { recipientAddrList += ", "; } recipientAddrList += makeAsString("<", info["fqName"], ">"); response = makeAsString("250 OK for ", info["fqName"], "\r\n"); send "writeBytes"(response) to connObj from nil; return (0); } SMTPclientConnection:_vrfy(array tokens) { string response; // 550 userName user unknown // 250 Geoff Carpenter oid mailbox = send "lookupMailbox"(tokens[1]) to mailboxService; if (mailbox == nil) { response = makeAsString("550 ", tokens[1], " user unknown\r\n"); send "writeBytes"(response) to connObj from nil; return (0); } assoc info = send "getMailboxInfo" to mailbox; logOutput(LOG_INFO, "info=", info); response = makeAsString("250 ", info["fullName"], " <", info["fqName"], ">\r\n"); send "writeBytes"(response) to connObj from nil; return (0); } SMTPclientConnection:_expn(array tokens) alias for _vrfy; SMTPclientConnection:_processMail() { oid mailbox; logOutput(LOG_INFO, "recipients=", emptySet + recipients); string mailMessage = makeAsString(mailBody); // convert to big string for mailbox in recipients do { send "storeMail"(mailMessage) to mailbox from nil; } string response = "250 OK\r\n"; // all done... send "writeBytes"(response) to connObj from nil; call "_reset"(); return (0); } SMTPclientConnection:systemShutdown() { string line = makeAsString("421 ", serverConfig["hostName"], " is shutting down\r\n"); int rc = send "writeBytes"(line) to connObj; send "deleteYourself" to readBfr; readBfr = nil; sessionClosed = 1; shutdownOID = nil; } /* vim: set expandtab shiftwidth=4 tabstop=4: */