/
ColdCore-3.0a9.02/
ColdCore-3.0a9.02/src/
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());
    }
};