// $Id: server.c,v 1.57 2008/05/24 07:23:00 lynx Exp $ // vim:syntax=lpc /* * rudimentary prototype of an SMTP server * all your protocols belong to us */ #include #include #include // does mailto:*place@host work for addressing places? // unfortunately # is not allowed in mailto: urls according to the NEW URI RFC #define GROUP_PREFIX '*' #define PERSON_PREFIX '~' // RFC 822 allows both * and ~ in usernames. phew. #ifndef MAX_EMAIL_LINES # ifdef _flag_optimize_protection_SMTP # define MAX_EMAIL_LINES 5 # else # define MAX_EMAIL_LINES 50 # endif #endif #ifndef MAX_EMAIL_SIZE # ifdef _flag_optimize_protection_SMTP # define MAX_EMAIL_SIZE 50 * 7 # else # define MAX_EMAIL_SIZE 50 * 70 # endif #endif #ifndef _time_logon_delay_SMTP # define _time_logon_delay_SMTP 20 #endif // handle st00pid optional brackets #define ADDRPARSE(XXX, USER, HOST) (\ sscanf(XXX, "<%s@%s>", USER, HOST) || \ sscanf(XXX, "%s@%s", USER, HOST) || \ sscanf(XXX, "<%s>", USER) || \ (USER = XXX) ) qScheme() { return "smtp"; } volatile mapping vars; volatile object target; volatile string origin, text, header; volatile int bodycount; // yeaaahhh!! bodycount's in the house!! volatile mapping head2vars = ([ "subject": "_subject", "content-type": "_type_content", "message-id": "_ID_SMTP", ]); #ifdef SMTP_ALIASES volatile mapping smtp_aliases = ([ SMTP_ALIASES ]); #endif // the most important thing in an smtp daemon is.. the spam filter // // our current strategy is: // reject mails with attachments. // reject non-plain mails. // reject mails that exceed MAX_EMAIL_LINES lines. // // leaves you with only the shortest V1AGRA candidates. // spam(mc, extra, message) { P1(("%O spam(%O, %O..\n", ME, mc, extra)) write(message); #if 0 monitor_report("_warning_abuse"+ mc, S("%O rejecting %s mail from %O for %O.", query_ip_number(), a, origin, vars["_nick_target"])); #else // seems to work generally, so lets reduce the // reporting to a logfile.. log_file("SMTP_SPAM", "%s\t%s\t%s\t%s\t%O\n", query_ip_number() || "(null)", vars["_nick_target"] || "(null)", mc, origin || "(null)", extra); #endif QUIT } parse(a) { /* * this is the parser for the command mode */ string cmd, fullargs; string *args; unless (sscanf(a, "%s%t%s", cmd, fullargs)) cmd = a; if (fullargs) args = explode(fullargs, " "); if (smtpcmd(cmd, args, fullargs, a)) next_input_to(#'parse); } #if !__EFUN_DEFINED__(trim) # define trim(x) x #endif body(a) { if (a != ".") { if (++bodycount > MAX_EMAIL_LINES) return spam("_illegal_length", vars["_subject"], "254 Insufficient storage for long messages\n"); if (a != "") { text += a + "\n"; if (strlen(text) > MAX_EMAIL_SIZE) return spam("_illegal_size", vars["_subject"], "254 Insufficient storage for long messages\n"); } next_input_to(#'body); return; } PT(("SMTP %O content %O vars %O\n", query_ip_number(), text, vars)) unless (objectp(target)) { // || (strlen(vars["_nick_target"]) && // (target = vars["_nick_target"][0] == GROUP_PREFIX // ? find_place(vars["_nick_target"]) : // summon_person(vars["_nick_target"])))) { // ... and PERSON_PREFIX? // re-summon may happen when a user logged out while the // mail was coming in. unlikely. even unlikelier that it // gets here: P0(("SMTP %O requires re-summon of username %O\n", query_ip_number(), user)) write("550 Technical problems summoning recipient\n"); // let's observe this.. does it really happen? // having summon logic twice in here is silly } else { string subject; text = trim(chomp(text)); subject = trim(vars["_subject"] || ""); if (strlen(text) < 3 && strlen(subject) < 3) { return spam("_illegal_content_empty", subject+text+a, "254 What is this? An SMTP Presence Notification? Nice, we don't have that, yet. We have to invent it first!\n"); } vars["_content"] = text; vars["_subject"] = decode_embedded_charset(subject); // more likely to have resolved by now // even better to call async DNS here.. one day vars["_host_source"] = query_ip_name(); write("250 accepted for delivery\n"); // certainly not the definitive mc, but at least person // doesn't reply with a _message_echo_private which // then goes out to spammers and godknowswho.. although // in some cases that's sexy. // // uncool about this one: it doesn't get logged. but // a _message wouldn't have the template. we need // the mc reform!! sendmsg(target, "_notice_email_delivered", //"([_method_SMTP] from [_host_source] to [_nick_target]@[_host_target]) [_subject]\n[_content]\n", // [_nick] wird erweitert um die source??!? "([_method_SMTP] from [_nick] to [_nick_target]) [_subject]\n[_content]\n", // "smtp:" for SOML-capable senders? vars, "mailto:"+ origin); // a log to get a clue if everything is working fine. // sendmail keeps such logs too log_file("SMTP_OK", "%s\t%s\t%s\t%s\n", vars["_host_source"] || "(null)", vars["_nick_target"] || "(null)", vars["_host_target"] || "(null)", origin || "(null)"); target = 0; vars = ([]); } return next_input_to(#'parse); } headers(a) { string t; if (a == "") { next_input_to(#'body); bodycount = 0; return; } next_input_to(#'headers); unless (sscanf(a, "%s:%.0t%s", t, a)) { // a continuation line.. if (header) { if (vars[header]) vars[header] += a; else { // this cannot happen really P0(("SMTP %O line cont for %O w/out start: %O\n", query_ip_number(), header, a)) } } else { // skip! this is a continuation of a header that // we have rejected according to head2vars! // so we get here continously and there's nothing wrong // with it! } return; } if (header = head2vars[lower_case(t)]) switch(header) { case "_type_content": unless (abbrev("text/plain", a)) return spam("_illegal_type_content", a, "254 Insufficient storage for non-plain messages\n"); // we don't need to store this one header = 0; // no line conts either break; // don't operate on headers yet as a line continuation may follow // case "_subject": // vars["_subject"] = decode_embedded_charset(a); // break; default: vars[header] = a; } } smtpcmd(cmd, args, all, asis) { object o, palo; string user, host, mail; P2(("smtpcmd %s %O\n", cmd, args)) switch(cmd = upper_case(cmd)) { case "RSET": origin = 0; target = 0; case "NOOP": write("250 SYS 64738\n"); break; case "HELO": case "EHLO": if (sizeof(args) == 1) { write("250 "+ SERVER_HOST +" Hello " + query_ip_number() +"\n"); // let's not put useless work on erq // +query_ip_name()+" ["+query_ip_number()+"].\n"; } else write("501 Syntax: HELO hostname\n"); break; case "MAIL": case "SEND": case "SOML": case "SAML": vars["_method_SMTP"] = cmd; if (upper_case(all[0..4]) == "FROM:") { origin = all[5] == ' ' ? all[6..] : all[5..]; host = query_ip_number(); if (ADDRPARSE(origin, user, host)) { vars["_nick"] = origin = user +"@"+ host; P3(("smtpcmd %s %O ยป %s\n", cmd, all, origin)) write("250 Sender superficially ok\n"); break; } } write("501 Syntax: MAIL FROM:
\n"); break; case "RCPT": // unless (origin) { // write("503 Need MAIL before RCPT\n"); // break; // } if (upper_case(all[0..2]) == "TO:") { target = all[3] == ' ' ? all[4..] : all[3..]; if (ADDRPARSE(target, user, host)) { #ifdef SMTP_ALIASES string rcpt; vars["_nick_target"] = user; if (user = smtp_aliases[rcpt = lower_case(user)]) { vars["_nick_target_alias"] = user; } else user = rcpt; #else vars["_nick_target"] = user; user = lower_case(user); #endif if (strlen(user)) { // find_person and legal_name are not necessary // as summon_person / find_place do all of that if (user[0] == GROUP_PREFIX) target = find_place(user[1..]); else if #ifdef _flag_disable_prefix_person_SMTP (target = summon_person(user)) #else (user[0] == PERSON_PREFIX && target = summon_person(user[1 ..])) #endif { if (target->isNewbie()) target = 0; } if (target) { vars["_host_target"] = host || ""; write("250 Recipient apparently ok\n"); break; } } target = 0; spam("_unknown_name_user", 0, "550 '"+user+"' unknown.\n"); // spam does a QUIT in this case. too harsh? break; } } write("501 Syntax: RCPT TO:
\n"); break; case "DATA": unless (origin && target) { spam("_insufficient_data", 0, "503 Watcha tryinta hack?\n"); // spam does a QUIT in this case. too harsh? break; } //write("354 Enter mail, end with '.' on a line by itself.\n"); write("354 Alright now, "+ query_ip_name() +", tell me the truth.\n"); // I'm reading your lips, baby. So you better not spam me. // But most of all the purpose is to activate erq for name // resolution at this point, since we will want the hostname // later. Of course we might as well go thru async DNS later // when we are absolutely sure we are going to keep the // message. TODO one day. header = text = ""; next_input_to(#'headers); return 0; case "QUIT": write("221 "+ SERVER_HOST +" says: I like your pink tie\n"); QUIT case "HELP": write("221 Ask http://about.psyc.eu/SMTP for help\n" "221 Commands: HELO, MAIL, SEND, SOML, SAML, RCPT, RSET, DATA, QUIT\n"); break; default: // some try binary overflow attacks here.. looks a bit ugly // but obviously has null effect on an ldmud P1(("SMTP %O got %O from %O\n", ME, asis, query_ip_number())) //write("500 Command not recognized.\n"); write("500 Huh?\n"); // why even talk to guys like that? QUIT break; } return 1; } greet() { remove_input_to(ME, #'quit); next_input_to(#'parse); vars = ([ ]); // should provide a hint for upgrading to PSYC protocol.. TODO write("220 ESMTP "+ SERVER_HOST +" makes me feel http://www.psyced.org\n" #ifdef _flag_optimize_protection_SMTP // multiline greetings is a SPAM deterrent provision. they are not // explicitely permitted in the RFC, but aren't forbidden either. // cheap SMTP implementations like python2.4/smtplib.py choke on this. # define MORE "220 " // 211? just as evil.. MORE "This is the psyced receptionist talking\n" MORE "You are best known as "+ query_ip_name() +"\n" MORE "Thank you for waiting. It's a spam deterrent provision\n" MORE "Somebody also said that multiline greetings embarrass spammers\n" MORE "If you like to spam, consider figure 1 at http://b-5.de/fig1/\n" # undef MORE #endif ); } logon() { P2(("SMTP logon from %O\n", query_ip_number())) #if DEBUG > 1 greet(); // make it easier for developers trying this out. #else call_out(#'greet, _time_logon_delay_SMTP); // now wait a minute! .. almost // this throws out spammers who think they can talk before // being greeted next_input_to(#'quit); #endif }