Search code examples
c#socketsemailsmtptcpclient

sending email using TcpClient


I'm trying to send an email with TcpClient in c#. and I dont know if that is possible or not.

******* I know I can use the SmtpClient but this is a homework and I just have to do it with sockets ******

I wrote this code:

TcpClient tcpclient = new TcpClient();

            // HOST NAME POP SERVER and gmail uses port number 995 for POP 

            //tcpclient.Connect("pop.gmail.com", 995);
            tcpclient.Connect("smtp.gmail.com", 465);
            // This is Secure Stream // opened the connection between client and POP Server
            System.Net.Security.SslStream sslstream = new SslStream(tcpclient.GetStream());
            // authenticate as client  
            //sslstream.AuthenticateAsClient("pop.gmail.com");
            sslstream.AuthenticateAsClient("smtp.gmail.com");
            //bool flag = sslstream.IsAuthenticated;   // check flag
            // Asssigned the writer to stream 
            System.IO.StreamWriter sw = new StreamWriter(sslstream);
            // Assigned reader to stream
            System.IO.StreamReader reader = new StreamReader(sslstream);
            // refer POP rfc command, there very few around 6-9 command
            sw.WriteLine("EHLO " + "smtp.gmail.com");
            sw.Flush();
            sw.WriteLine("AUTH LOGIN/r/n");
            sw.Flush();
            sw.WriteLine("******@gmail.com/r/n");
            sw.Flush();
            // sent to server
            sw.WriteLine("***********/r/n");
            sw.Flush();
            //// this will retrive your first email
            //sw.WriteLine("RETR 1");
            //sw.Flush();
            //// close the connection
            //sw.WriteLine("Quit ");
            //sw.Flush();
            sw.WriteLine("MAIL FROM:<" + "******@gmail.com" + ">\r\n");
            sw.Flush();
            sw.WriteLine("RCPT TO:<" + "*******@***.com" + ">\r\n");
            sw.Flush();
            sw.WriteLine("DATA\r\n");
            sw.Flush();
            sw.WriteLine("Subject: Email test\r\n");
            sw.Flush();
            sw.WriteLine("Test 1 2 3\r\n");
            sw.Flush();
            sw.WriteLine(".\r\n");
            sw.Flush();
            sw.WriteLine("QUIT\r\n");
            sw.Flush();

            string str = string.Empty;
            string strTemp = string.Empty;
            while ((strTemp = reader.ReadLine()) != null)
            {
                // find the . character in line
                if (strTemp == ".")
                {
                    break;
                }
                if (strTemp.IndexOf("-ERR") != -1)
                {
                    break;
                }
                str += strTemp;
            }
        }

the message which reader gets is:

"250-smtp.gmail.com at your service, [151.238.124.27]\r\n250-SIZE 35882577\r\n250-8BITMIME\r\n250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\r\n250-ENHANCEDSTATUSCODES\r\n250-PIPELINING\r\n250-CHUNKING\r\n250 SMTPUTF8\r\n451 4.5.0 SMTP protocol violation, see RFC 2821 x17-v6sm5346253edx.53 - gsmtp\r\n"

Any idea which part is wrong so Icould send an email via TcpClient?

So if that cannot happen, how can I send an email using sockets?


Solution

  • The problem is that you are not following the SMTP protocol correctly, as described in RFC 5321, RFC 2920, and RFC 2554.

    This is clear by the error message you are receiving from the server in reply to the AUTH LOGIN command:

    451 4.5.0 SMTP protocol violation, see RFC 2821 x17-v6sm5346253edx.53 - gsmtp

    Specifically,

    • several of your commands are being terminated by /r/n, which is both wrong (should be \r\n) and redundant (since you are using WriteLine(), which sends \r\n for you).

    • you are sending a bunch of SMTP commands without reading any of the responses in between each command. This is known as Command Piplining. However, you are not checking the server's EHLO response to make sure the server even allows pipelining. You can't pipeline commands unless the server tells you it is OK first.

    • you are not reading the responses correctly. Whether you use pipelining or not, SMTP responses are in a specific format, as outlined in RFC 5321 Section 4.2. Your reading code is not following that format, not even close.

    • you are not authenticating with the SMTP server correctly. In particular, the values you send to the server must be encoded in UTF-8 and base64. And you need to pay attention to the server's prompts to know when to send the username and when to send the password. Some servers do not require both values.

    With that said, try something more like this instead:

    private System.IO.StreamReader reader;
    private System.IO.StreamWriter writer;
    
    public class SmtpCmdFailedException : Exception
    {
        public int ReplyCode;
    
        public SmtpCmdFailedException(int code, string message)
            : base(message)
        {
            ReplyCode = code;
        }
    }
    
    private int readResponse(ref string replyText, params int[] expectedReplyCodes)
    {
        string line = reader.ReadLine();
        if (line == null)
            throw new EndOfStreamException();
    
        // extract the 3-digit reply code
        string replyCodeStr = line.Substring(0, 3);
    
        // extract the text message, if any
        replyText = line.Substring(4);
    
        // check for a multi-line response
        if ((line.Length > 3) && (line[3] == '-'))
        {
            // keep reading until the final line is received
            string contStr = replyCodeStr + "-";
            do
            {
                line = reader.ReadLine();
                if (line == null)
                    throw new EndOfStreamException();    
                replyText += "\n" + line.Substring(4);
            }
            while (line.StartsWith(contStr));
        }
    
        int replyCode = Int32.Parse(replyCodeStr);
    
        // if the caller expects specific reply code(s), check
        // for a match and throw an exception if not found...
        if (expectedReplyCodes.Length > 0)
        {
            if (Array.IndexOf(expectedReplyCodes, replyCode) == -1)
                throw new SmtpCmdFailedException(replyCode, replyText);
        }
    
        // return the actual reply code that was received
        return replyCode;
    }
    
    private int readResponse(params int[] expectedReplyCodes)
    {
        string ignored;
        return readResponse(ignored, expectedReplyCodes);
    }
    
    private int sendCommand(string command, ref string replyText, params int[] expectedReplyCodes)
    {
        writer.WriteLine(command);
        writer.Flush();
        return readResponse(replyText, expectedReplyCodes);
    }
    
    private int sendCommand(string command, params int[] expectedReplyCodes)
    {
        string ignored;
        return sendCommand(command, ignored, expectedReplyCodes);
    }
    
    void doAuthLogin(string username, string password)
    {
        // an authentication command returns 235 if authentication
        // is finished successfully, or 334 to prompt for more data.
        // Anything else is an error...
    
        string replyText;
        int replyCode = sendCommand("AUTH LOGIN", replyText, 235, 334);
    
        if (replyCode == 334)
        {
            // in the original spec for LOGIN (draft-murchison-sasl-login-00.txt), the
            // username prompt is defined as 'User Name' and the password prompt is
            // defined as 'Password'. However, the spec also mentions that there is at
            // least one widely deployed client that expects 'Username:' and 'Password:'
            // instead, and those are the prompts that most 3rd party documentations
            // of LOGIN describe.  So we will look for all known prompts and act accordingly.
            // Also throwing in 'Username' just for good measure, as that one has been seen
            // in the wild, too...
    
            string[] challenges = new string[]{"Username:", "User Name", "Username", "Password:", "Password"};
    
            do
            {
                string challenge = Encoding.UTF8.GetString(Convert.FromBase64String(replyText));
    
                switch (Array.IndexOf(challenges, challenge))
                {
                    case 0:
                    case 1:
                    case 2:
                        replyCode = sendCommand(Convert.ToBase64String(Encoding.UTF8.GetBytes(username)), replyText, 235, 334);
                        break;
    
                    case 3:
                    case 4:
                        replyCode = sendCommand(Convert.ToBase64String(Encoding.UTF8.GetBytes(password)), replyText, 235, 334);
                        break;
    
                    default:
                        throw new SmtpCmdFailedException(replyCode, replyText);
                }
            }
            while (replyCode == 334);
        }
    }
    
    ...
    
    TcpClient tcpclient = new TcpClient();
    
    tcpclient.Connect("smtp.gmail.com", 465);
    
    // implicit SSL is always used on SMTP port 465
    System.Net.Security.SslStream sslstream = new SslStream(tcpclient.GetStream());
    sslstream.AuthenticateAsClient("smtp.gmail.com");
    //bool flag = sslstream.IsAuthenticated;   // check flag
    
    writer = new StreamWriter(sslstream);
    reader = new StreamReader(sslstream);
    
    string replyText;
    string[] capabilities = null;
    string[] authTypes = null;
    
    // read the server's initial greeting
    readResponse(220);
    
    // identify myself and get the server's capabilities
    if (sendCommand("EHLO myClientName", replyText) == 250)
    {
        // parse capabilities
        capabilities = replyText.Split(new Char[]{'\n'});
        string auth = Array.Find(capabilities, s => s.StartsWith("AUTH ", true, null));
        authTypes = auth.Substring(5).Split(new Char[]{' '});
    
        // authenticate as needed...
        if (Array.IndexOf(authTypes, "LOGIN") != -1)
            doAuthLogin("******@gmail.com", "***********");
    }
    else
    {
        // EHLO not supported, have to use HELO instead, but then
        // the server's capabilities are unknown...
    
        capabilities = new string[]{};
        authTypes = new string[]{};
    
        sendCommand("HELO myclientname", 250);
    
        // try to authenticate anyway...
        doAuthLogin("******@gmail.com", "***********");
    }
    
    // check for pipelining support... (OPTIONAL!!!)
    if (Array.IndexOf(capabilities, "PIPELINING") != -1)
    {
        // can pipeline...
    
        // send all commands first without reading responses in between
        writer.WriteLine("MAIL FROM:<" + "******@gmail.com" + ">");
        writer.WriteLine("RCPT TO:<" + "*******@***.com" + ">");
        writer.WriteLine("DATA");
        writer.Flush();
        
        // now read the responses...
    
        Exception e = null;
    
        // MAIL FROM
        int replyCode = readResponse(replyText);
        if (replyCode != 250)
            e = new SmtpCmdFailedException(replyCode, replyText);
    
        // RCPT TO
        replyCode = readResponse(replyText);
        if ((replyCode != 250) && (replyCode != 251) && (e == null))
            e = new SmtpCmdFailedException(replyCode, replyText);
    
        // DATA
        replyCode = readResponse(replyText);
        if (replyCode == 354)
        {
            // DATA accepted, must send email followed by "."
            writer.WriteLine("Subject: Email test");
            writer.WriteLine("Test 1 2 3");
            writer.WriteLine(".");
            writer.Flush();
    
            // read the response
            replyCode = readResponse(replyText);
            if ((replyCode != 250) && (e == null))
                e = new SmtpCmdFailedException(replyCode, replyText);
        }
        else
        {
            // DATA rejected, do not send email
            if (e == null)
                e = new SmtpCmdFailedException(replyCode, replyText);
        }
    
        if (e != null)
        {
            // if any command failed, reset the session
            sendCommand("RSET");
            throw e;
        }
    }
    else
    {
        // not pipelining, MUST read each response before sending the next command...
    
        sendCommand("MAIL FROM:<" + "******@gmail.com" + ">", 250);
        try
        {
            sendCommand("RCPT TO:<" + "*******@***.com" + ">", 250, 251);
            sendCommand("DATA", 354);
            writer.WriteLine("Subject: Email test");
            writer.WriteLine("");
            writer.WriteLine("Test 1 2 3");
            writer.Flush();
            sendCommand(".", 250);
        }
        catch (SmtpCmdFailedException e)
        {
            // if any command failed, reset the session
            sendCommand("RSET");
            throw;
        }
    }
    
    // all done
    sendCommand("QUIT", 221);