使用 TcpClient 发送电子邮件

sending email using TcpClient

我正在尝试使用 C# 中的 TcpClient 发送电子邮件。 我不知道这是否可能。

******* 我知道我可以使用 SmtpClient 但这是一项作业,我只需要使用套接字就可以了 ******

我写了这段代码:

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;
            }
        }

reader收到的消息是:

"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"

知道哪一部分是错误的,所以我可以通过 TcpClient 发送电子邮件吗?

所以如果那不可能发生,我如何使用套接字发送电子邮件?

问题是您没有正确遵循 SMTP 协议,如 RFC 5321, RFC 2920, and RFC 2554 中所述。

您从服务器收到的回复 AUTH LOGIN 命令的错误消息清楚地表明了这一点:

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

具体来说,

  • 您的一些命令被 /r/n 终止,这既是错误的(应该是 \r\n)又是多余的(因为您使用的是 WriteLine(),为您发送 \r\n)。

  • 您正在发送一堆 SMTP 命令,但没有读取每个命令之间的任何响应。这被称为 Command Piplining。但是,您没有检查服务器的 EHLO 响应以确保服务器甚至允许流水线操作。除非服务器首先告诉您它可以,否则您不能通过管道传输命令。

  • 您没有正确阅读回复。无论您是否使用流水线,SMTP 响应都采用特定格式,如 RFC 5321 Section 4.2 中所述。您的阅读代码不符合该格式,甚至不符合。

  • 您没有正确验证 SMTP 服务器。特别是,您发送到服务器的值必须以 UTF-8 和 base64 编码。并且需要注意服务器的提示,知道什么时候发送用户名,什么时候发送密码。一些服务器不需要这两个值。

话虽如此,请尝试更类似的方法:

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);