使用 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);
我正在尝试使用 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);