new object $smtp: $network; var $root created_on = 845213524; var $root defined_settings = #[["aliases", #[['get, ['get_aliases_setting]], ['set, ['set_aliases_setting]], ['parse, ['parse_itemlist, 'parse_alias_setting]], ['format, ['fmt_aliases_setting]]]], ["hosts", #[['get, ['get_hosts_setting]], ['set, ['set_hosts_setting]], ['parse, ['parse_itemlist, 'parse_host_setting]], ['format, ['format_itemlist]]]], ["maildrop", #[['get, ['get_maildrop_setting]], ['set, ['set_maildrop_setting]], ['parse, ['parse_maildrop_setting]], ['format, ['fmt_maildrop_setting]]]]]; var $root flags = ['variables, 'methods, 'code, 'core]; var $root inited = 1; var $root managed = [$smtp]; var $root manager = $smtp; var $root trusted = [$login_interface, $user_info]; var $root trusted_by = [$outbound_connection]; var $smtp hosts = 0; var $smtp mail_aliases = #[["postmaster", $mail_postmaster], ["admin", $mail_admin], ["abuse", $mail_admin], ["MAILER-DAEMON", $mail_postmaster]]; var $smtp maildrop = "127.0.0.1"; var $smtp postmaster = 0; var $smtp timeouts = #[]; var $smtp valid_email_regexp = "^[-a-z0-9_!.%+$'=]+[^.]$"; protected method .DATA() { arg c, body; var lines, code, line, i; .set_timeout(c, 120); c.write("DATA"); lines = (> .get_response(c) <); [code, line] = lines[1]; if (code != 354) throw(~error, (lines.slice(2)).join(), code); // escape period DATA termination while ((i = find i in (body) where (i == "."))) body = replace(body, i, ".."); // send it c.write(body + ["."]); lines = (> .get_response(c) <); [code, line] = lines[1]; if (code != 250) throw(~error, (lines.slice(2)).join(), code); }; protected method .MAIL_FROM() { arg c, address; var lines, code, line; .set_timeout(c, 120); c.write(("MAIL FROM: <" + address) + ">"); lines = (> .get_response(c) <); [code, line] = lines[1]; if (code == 250) return lines.slice(2); throw(~error, (lines.slice(2)).join(), code); }; protected method .QUIT() { arg c; var lines, code, line, i; .set_timeout(c, 120); c.write("QUIT"); // get it, but ignore it lines = (> .get_response(c) <); }; protected method .RCPT_TO() { arg c, address; var lines, code, line; .set_timeout(c, 120); c.write(("RCPT TO: <" + address) + ">"); lines = (> .get_response(c) <); [code, line] = lines[1]; if ((code < 300) && (code >= 200)) return [code, lines.slice(2)]; throw(~error, (lines.slice(2)).join(), code); }; protected method .RSET() { arg c; var lines, code, line, i; .set_timeout(c, 120); c.write("RSET"); // get it, but ignore it lines = (> .get_response(c) <); }; protected method .VRFY() { arg c, address; var lines, code, line; .set_timeout(c, 120); c.write("VRFY " + address); lines = (> .get_response(c) <); [code, line] = lines[1]; if ((code < 300) && (code >= 200)) return [code, lines.slice(2)]; throw(~error, (lines.slice(2)).join(), code); }; public method .add_mail_alias() { arg alias, recip; var name; if (type(alias) != 'string) throw(~type, "First argument must be a string name for the alias"); if ((type(recip) != 'objnum) || (!(recip.is($mail_list)))) throw(~type, "Second argument is not a valid $mail_list"); name = alias; name = strsed(alias, "[^a-z0-9\.-]+", "", "g"); if (name != alias) throw(~type, "Alias may only be alphanumeric characters, or a period or dash"); mail_aliases = dict_add(mail_aliases, alias, recip); }; public method .allowed() { arg cmd; return 1; }; protected method .close_session() { arg c; (| c.close() |); .set_timeout(c, 0); }; protected method .connect_to_smtp_host() { arg ip, @host; var line, c; if (host) host = host[1]; else host = ip; c = $outbound_connection.new(); catch any { c.open_connection(ip, 25); (> .negotiate_connect(c) <); } with { (| c.close() |); if (error() == ~refused) throw(~refused, "Unable to open SMTP connection to " + host); else rethrow(error()); } return c; }; public method .connection_going_away() { arg addr, port; }; public method .connection_starting() { arg addr, port; }; root method .core_smtp(): nooverride { maildrop = "127.0.0.1"; hosts = 0; }; public method .del_mail_alias() { arg alias; return dict_del(mail_aliases, alias); }; public method .fmt_aliases_setting() { arg data; var a; return map a in (data) to (((a[1]) + ":") + ((a[2]).mail_name())).join(", "); }; public method .fmt_maildrop_setting() { arg data; return (| (($dns.hostname(data)) + "/") + data |) || data; }; public method .format_email_address() { arg recip, @notfull; var host, mailname; host = (| (.get_hosts_setting())[1] |); mailname = recip.mail_name(); if ((| .lookup_alias((recip.name()).replace(" ", "-")) |)) { mailname = strsed(mailname, "^[*~]", ""); } else { mailname = strsub(recip.mail_name(), "*", "list-"); mailname = strsub(mailname, "~", "user-"); } if (host) { if (notfull) return (mailname + "@") + host; return ((((((recip.name()).capitalize()) + " <") + mailname) + "@") + host) + ">"; } else { if (notfull) return mailname; return ((((recip.name()).capitalize()) + " <") + mailname) + ">"; } }; protected method .get_aliases_setting() { arg @args; return mail_aliases; }; public method .get_email_address() { arg type; var email, r, host; if (type(type) != 'string) type = tostr(type); return .format_email_address((| .parse_recipient(type) |) || $mail_admin); }; public method .get_hosts_setting() { arg @args; return hosts || [$dns.hostname("")]; }; public method .get_maildrop_setting() { arg @args; return maildrop; }; protected method .get_response() { arg c; var line, code, m, more, out; if (c.is_reading_block()) throw(~engaged, "Connection is already reading."); more = 1; out = []; while (more) { line = (c.start_reading_block('one))[1]; if (line == 'aborted) throw(~aborted, "Connection read aborted."); m = regexp(line, "^([0-9]+)([- ])(.*)$"); if (listlen(m) != 3) throw(~invalid, "Unexpected response from server: " + line); more = (m[2]) == "-"; out += [[toint(m[1]), m[3]]]; } return out; }; public method .get_system_email() { arg type; var email, r, host; if (type(type) != 'string) type = tostr(type); // format aliases here, so .format_email_address() doesn't confuse them if ((r = (| .lookup_alias(type) |))) { if ((host = (| (.get_hosts_setting())[1] |))) return (type + "@") + host; return type; } if ((r = (| .parse_recipient(type) |))) return .format_email_address(r, 'notfull); r = $mail_admin; if ((host = (| (.get_hosts_setting())[1] |))) return "admin@" + host; return "admin"; }; public method .lookup_alias() { arg name; return (> mail_aliases[name] <); }; public method .mail_aliases() { return mail_aliases; }; protected method .negotiate_connect() { arg c; var lines, code, line; .set_timeout(c, 120); lines = (> .get_response(c) <); [code, line] = lines[1]; if (code == 421) throw(~notavail, (lines.slice(2)).join()); if (code != 220) throw(~invalid, (("Unknown response from server: " + code) + " ") + line); .set_timeout(c, 120); c.write("HELO " + ($sys.server_info('server_hostname))); lines = (> .get_response(c) <); [code, line] = lines[1]; if (code == 250) return; if ((code % 500) < 100) throw(~syntax, "I'm doing something wrong: " + line); if (code == 421) throw(~notavail, (lines.slice(2)).join()); throw(~invalid, (("Unknown response from server: " + code) + " ") + line); }; public method .parse_alias() { arg alias; var tmp, obj, o; tmp = explode(alias, ":"); if (listlen(tmp) != 2) throw(~type, ("Invalid alias pair '" + alias) + "', should be ALIAS:OBJ"); [alias, obj] = tmp; tmp = strsed(alias, "[^a-z0-9\.-]+", "", "g"); if (tmp != alias) throw(~type, "Alias must be composed of a-z, 0-9, a period or a dash"); if (obj && ((obj[1]) == "$")) { if (!(o = (| $object_lib.to_dbref(obj) |))) throw(~type, "Invalid object: " + obj); } else if (!(o = (| $mail_lib.match_mail_recipient(obj) |))) { throw(~type, "Invalid mail recipient: " + obj); } if (!(o.is($mail_list))) throw(~type, ("Object " + o) + " is not a valid mail recipient."); return [alias, o]; }; public method .parse_alias_setting() { arg value, action, @args; var a; if (action == 'del) { value = explode(value, ":")[1]; a = strsed(value, "[^a-z0-9\.-]+", "", "g"); if (a != value) throw(~type, "Alias must be composed of a-z, 0-9, a period or a dash"); if (!dict_contains(mail_aliases, value)) throw(~failed, ("The alias '" + value) + "' is not set, and thus cannot be removed"); return value; } else { return (> .parse_alias(value) <); } }; public method .parse_email_address() { arg address, @lookup; var name, host, ip, h; if (!address) throw(~invemail, "No email address supplied."); address = explode(address, "@"); if (listlen(address) != 2) throw(~invemail, ("\"" + (address.join("@"))) + "\" is not a valid email address."); [name, host] = address; if (!match_regexp(name, valid_email_regexp)) throw(~invemail, ("'" + name) + "' is not a valid Internet mail username"); if (lookup) { ip = $dns.ip(host); if ((!ip) || (ip == "-1")) throw(~invemail, ("'" + host) + "' does not resolv to an IP address"); h = $dns.hostname(ip); if (!($dns.valid_hostname(h))) throw(~invemail, (("Invalid DNS entry for: " + host) + "/") + ip); host = h; } else if (!($dns.valid_hostname(host))) { throw(~invemail, ("Invalid hostname \"" + host) + "\""); } return [name, host, ip]; }; public method .parse_host_setting() { arg value, action, @args; if (action == 'del) { if (!(value in hosts)) throw(~failed, ("The host '" + value) + "' is not set, and thus cannot be removed"); return value; } else { (> $dns.ip(value) <); return lowercase(value); } }; public method .parse_maildrop_setting() { arg value, @args; var ip; ip = explode(ip, "/")[1]; ip = (| $dns.ip(value) |) || ip; catch any { $dns.hostname(ip); } with { if (error() == ~invip) rethrow(error()); } return ip; }; public method .parse_recipient() { arg user; var x, host; if (((user[1]) == "<") && ((user[user.length()]) == ">")) user = user.subrange(2, (user.length()) - 2); if ("@" in user) { [user, host] = explode(user, "@"); if (!(host in ($smtp.get_hosts_setting()))) throw(~perm, "Unwilling to accept or relay mail for: " + host, 571); } user = user.trim(); user = strsed(user, "^list-", "*"); user = strsed(user, "^user-", "~"); return (| $smtp.lookup_alias(user) |) || (> $mail_lib.match_mail_recipient(user) <); }; public method .remove_aliases() { var x; (> .perms(caller(), $mail_list) <); for x in (mail_aliases) { if ((x[2]) == sender()) mail_aliases = dict_del(mail_aliases, x[1]); } }; public method .sendmail() { arg from, recip, subj, @body; var mailagent, header, c; (> .perms(sender(), 'trusts) <); mailagent = ((((($motd.server_name()) + " (") + ($sys.server_info('server_hostname))) + ":") + ($login_daemon.current_port())) + ")"; (> .parse_email_address(recip) <); if (!from) from = 'postmaster; if (type(from) == 'symbol) from = .get_email_address(from); header = ["Date: " + ($time.format("%a, %d %b %Y %T %Z")), "From: " + from, "To: " + recip, "Subject: " + subj, "Errors-to: " + (.get_email_address('postmaster)), "X-Mail-Agent: " + mailagent]; // connect to maildrop c = (> .connect_to_smtp_host(maildrop) <); // send it off catch any { (> .MAIL_FROM(c, from) <); (> .RCPT_TO(c, recip) <); (> .DATA(c, (header + [""]) + body) <); (> .QUIT(c) <); .close_session(c); } with { .close_session(c); rethrow(error()); } }; protected method .set_aliases_setting() { arg name, definer, value; switch (value[1]) { case 'set: mail_aliases = value[2]; case 'add: mail_aliases = dict_add(mail_aliases, @value[2]); case 'del: mail_aliases = dict_del(mail_aliases, value[2]); default: throw(~type, "Unknown action: " + (value[1])); } }; protected method .set_hosts_setting() { arg name, definer, value; switch (value[1]) { case 'set: hosts = value[2]; case 'add: hosts = setadd(hosts, value[2]); case 'del: hosts = setremove(hosts, value[2]); default: throw(~type, "Unknown action: " + (value[1])); } }; public method .set_maildrop_setting() { arg name, definer, value; maildrop = value; }; protected method .set_timeout() { arg c, timeout; var task; task = (| timeouts[task_id()] |); if (task) (| $scheduler.del_task(task) |); if (timeout) { task = $scheduler.add_task(timeout, 'timeout, [c]); timeouts = dict_add(timeouts || #[], task_id(), task); } else if (dict_contains(timeouts || #[], task_id())) { timeouts = dict_del(timeouts, task_id()); } }; protected method .timeout() { arg conn; (| conn.close() |); }; public method .verify_email_address() { arg address; var c, lines, code, email, name, m; if (!(| .perms(caller(), 'trusts) |)) (> .perms(sender(), 'trusts) <); // connect address = (> .parse_email_address(address, 'resolv_host) <); c = (> .connect_to_smtp_host(address[3], address[2]) <); // VRFY enabled? catch ~error { [code, lines] = (> .VRFY(c, address[1]) <); if (code == 250) { .QUIT(c); .close_session(c); // munch on it a little if ((m = regexp(lines[1], "^([^<]+)<([^>]+)>"))) { [name, email] = m; } else if ((m = regexp(lines[1], "<([^>]+)>"))) { email = m[1]; name = strsed(lines[1], " *<[^>]+> *", ""); } else { [email, (name ?= "")] = lines; } return [email.trim(), name.trim()]; } } // nope, try RCPT.. catch any { (> .MAIL_FROM(c, $smtp.get_email_address('postmaster)) <); [code, lines] = (> .RCPT_TO(c, address[1]) <); .QUIT(c); .close_session(c); if ((m = regexp(lines[1], "<([^>]+)>"))) email = m[1]; else email = lines[1]; return [email.trim()]; } with { .QUIT(c); .close_session(c); rethrow(error()); } };