Search code examples
socketsemailresolvesmtps

mail submission agent (MSA) server hostname from email address via c++


I am looking for a generic way to get the hostname of mail submission servers by only having the email address with c/c++, so I can connect to port 587 and/or 465 smtp submission.

E.g. [email protected] -> smtp.gmail.com or gmail-smtp-msa.l.google.com

Basically I am sending some specific emails on behalf of clients. So at the end it looks like they have sent the email. Up to now I am still using a simple domain to hostname map like {gmail.com,smtp.gmail.com} which I extend whenever needed. However I would very much like to avoid this map and automate this process.

A while ago I posted c++ sockets - smtp server hostname from email address, which should have been the same question, maybe it was not clear enough. In the answer I was advised to perform DNS lookup and ask for MX records. A book and an implementation later I noticed that these mx records are not suitable for mail submission (can't connect to port 587, only 25). I checked all other types defined in resolv.h and checked nslookup (which is probably using resolv.h anyway) and by now I really think this is not doable via DNS lookup. If somebody wants to try it, check nslookup -type=mx gmail.com you will not see any -msa... stuff.

I am a bit desperate now, because I really want to get it done. However I also dont want to spend weeks on it. I would be also really happy about an answer telling me that it is not doable in a reasonable amount of time.


EDIT:

Following a minimal example I just made very quick via the DNS MX approach and where i tried to connect to ports 25, 465, 587.

/*c++1z, Don't forget -lresolv*/
#include <resolv.h>
#include <string>
#include <map>
#include <sstream>
#include <cerrno>
#include <netdb.h>
#include <iostream>
#include <cstddef>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/tcp.h>

//like assert but throws instead
#define CHECK(condition)\
    if(!(condition)){\
        std::ostringstream oss;\
        oss << __FILE__ << ":" << __LINE__ << "\nerrno:" << errno << "\nh_errno:" << h_errno << std::endl;\
        throw std::runtime_error(oss.str());\
    }

//!
//! \brief mxRecords performs dns T_MX lookup
//! \param domain the domain part of the email e.g. [email protected] -> gmail.com
//! \return multimap of {priority, server hostname} pairs
//!
auto mxRecords(std::string const&domain)
->std::multimap<size_t/*priority*/,std::string/*server name*/>
{
    std::basic_string<unsigned char> response(PACKETSZ,'0');
    int response_size = res_search(domain.data(),C_IN,T_MX,response.data(),response.size());
    CHECK(response_size != -1);
    response.resize(response_size);

    ns_msg handle;
    CHECK(ns_initparse(response.data(),response.size(),&handle) != -1);

    std::multimap<size_t,std::string> ret;
    for(int i = 0; i < ns_msg_count(handle,ns_s_an); ++i)
    {
        ns_rr rr;
        if(ns_parserr(&handle,ns_s_an,i,&rr) == -1)
            continue;
        if(ns_rr_type(rr) != ns_t_mx)
            continue;
        std::string exp_dn(MAXDNAME,0);
        if(ns_name_uncompress(ns_msg_base(handle),ns_msg_end(handle),ns_rr_rdata(rr)+NS_INT16SZ,exp_dn.data(),exp_dn.size()) == -1)
            continue;
        ret.emplace(ns_get16(ns_rr_rdata(rr)),exp_dn.data());
    }
    return ret;
}
bool __connect(std::string const&host, std::string const&port)
{
    addrinfo const __req{0,AF_UNSPEC,SOCK_STREAM,IPPROTO_TCP,0,nullptr,nullptr,nullptr};
    addrinfo *__pai(nullptr);
    if(getaddrinfo(host.data(),port.data(),&__req,&__pai) != 0)
        return false;
    for(addrinfo *iter = __pai; iter != nullptr; iter = iter->ai_next)
    {
        int fd = socket(iter->ai_family, iter->ai_socktype, iter->ai_protocol);
        if(fd == -1)
            continue;
        int retries = 1;
        setsockopt(fd, IPPROTO_TCP, TCP_SYNCNT, &retries, sizeof(retries));
        if(::connect(fd, iter->ai_addr, iter->ai_addrlen) == -1)
        {
            close(fd);
            continue;
        }
        freeaddrinfo(__pai);
        close(fd);
        return true;
    }
    freeaddrinfo(__pai);
    return false;
}
int main()
{
    std::string domain = "gmail.com";

    //get mx records and try to connect for port 25,465,587
    auto mx_records = mxRecords(domain);
    std::cout << "----------MX RECORDS <priority|host|port25|port465|port587> for " << domain << std::endl;
    for(auto iter = mx_records.begin(); iter != mx_records.end(); ++iter)
    {
        std::cout << iter->first << " | " << iter->second;
        //try port 25|port 465 | port 587
        for(auto port : {"25","465","587"})
        {
            if(__connect(iter->second,port))
                std::cout << " | y";
            else
                std::cout << " | n";
        }
        std::cout << std::endl;
    }
    return 0;
}

gives as output

----------MX RECORDS <priority|host|port25|port465|port587> for gmail.com
5 | gmail-smtp-in.l.google.com | y | n | n
10 | alt1.gmail-smtp-in.l.google.com | y | n | n
20 | alt2.gmail-smtp-in.l.google.com | y | n | n
30 | alt3.gmail-smtp-in.l.google.com | y | n | n
40 | alt4.gmail-smtp-in.l.google.com | y | n | n

Please note the "in" inside these hostnames. I made a dns query for cname of smtp.gmail.com and it gives me gmail-smtp-msa.l.google.com so "msa", which is open for ports 587/465. I really think these mx records are nowadays only good for port 25 and not for mail submission.


EDIT #2:

There is a lot of confusion about ports 25, 465, 587 on internet. Just to clear things up for me these ports don't differ in ssl/tls a priory. Port 25 is e.g. when you want to send a email from [email protected] to [email protected]. Port 587 is e.g. when you want to send a email from [email protected] to [email protected]. Finally port 465[deprecated] is just the same as 587 with the addition that it starts already with ssl/tls. Please correct me if I am wrong, but I am pretty sure I am not! Following the logs of 3 EHLO runs from my real application. First using a mx record hostname and port 25, second using "smtp.gmail.com" and port 587 and last one using "smtp.gmail.com" and port 465. Notice the difference for 587/465 having an AUTH command option after secure connection and 25 having no AUTH option. So when sending emails over port 25 reciever gets informed that sender is not verified and you usually get blocked (SPAM) soon. Anyway I need this AUTH command, application is totally legal no spamming etc. and this is the reason why I need smtp servers which have port 587 open and I dont think these MX servers are the correct ones.

PORT-25

S:220 mx.google.com ESMTP ############### - gsmtp
C:EHLO ###############
S:250-mx.google.com at your service, ###############
  250-SIZE 157286400
  250-8BITMIME
  250-STARTTLS
  250-ENHANCEDSTATUSCODES
  250-PIPELINING
  250-CHUNKING
  250 SMTPUTF8
C:STARTTLS
S:220 2.0.0 Ready to start TLS
C:EHLO ###############
S:250-mx.google.com at your service, ###############
  250-SIZE 157286400
  250-8BITMIME
  250-ENHANCEDSTATUSCODES
  250-PIPELINING
  250-CHUNKING
  250 SMTPUTF8
C:QUIT
S:221 2.0.0 closing connection ############### - gsmtp

PORT-587

S:220 smtp.gmail.com ESMTP ############### - gsmtp
C:EHLO ###############
S:250-smtp.gmail.com at your service, ###############
  250-SIZE 35882577
  250-8BITMIME
  250-STARTTLS
  250-ENHANCEDSTATUSCODES
  250-PIPELINING
  250-CHUNKING
  250 SMTPUTF8
C:STARTTLS
S:220 2.0.0 Ready to start TLS
C:EHLO ###############
S:250-smtp.gmail.com at your service, ###############
  250-SIZE 35882577
  250-8BITMIME
  250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
  250-ENHANCEDSTATUSCODES
  250-PIPELINING
  250-CHUNKING
  250 SMTPUTF8
C:QUIT
S:221 2.0.0 closing connection ############### - gsmtp

PORT-465

S:220 smtp.gmail.com ESMTP ############### - gsmtp
C:EHLO ###############
S:250-smtp.gmail.com at your service, ###############
  250-SIZE 35882577
  250-8BITMIME
  250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
  250-ENHANCEDSTATUSCODES
  250-PIPELINING
  250-CHUNKING
  250 SMTPUTF8
C:QUIT
S:221 2.0.0 closing connection ############### - gsmtp

Solution

  • This is not technically required to be supported, but per RFC 6186 Sec. 3.1 you should query for SRV records for the _submission host in the _tcp subdomain, like so:

    $ nslookup -type=srv _submission._tcp.gmail.com
    Server:  google-public-dns-a.google.com
    Address:  8.8.8.8
    
    Non-authoritative answer:
    _submission._tcp.gmail.com      SRV service location:
              priority       = 5
              weight         = 0
              port           = 587
              svr hostname   = smtp.gmail.com
    

    Some things you should keep in mind:

    1. This is a "convenience feature", in a sense. If you have an e-mail address [email protected], then acme.com may (or may not) expose these RFC 6186 records in _tcp.acme.com to allow their users' mail client program to automatically discover services such as POP3, IMAP etc.
    2. There may be special restrictions imposed on connecting to a domain MSA. For example, the services may simply refuse connections from machines outside of xyz.com internal network, or require an internally trusted certificate, and so on. The MSA is intended for interacting with the mail client, MUA, not for general mail delivery.