使用Socket类基于SMTP协议发送邮件的Java代码故障排查
问题:手动实现SMTP邮件发送时持续收到250响应码导致发送失败
我正在开发一个基于SMTP协议发送邮件的Java类,不使用任何简化邮件发送的库(如MimeMessage或MailUtility)。以下是目前完成的代码:
public class MailSender { protected String localMachine; protected String localSMTPServer; private String textMail; private String mailFrom; public MailSender(String localSMTPServer, String mailFrom) { try { this.textMail = "null"; this.localSMTPServer = localSMTPServer; this.mailFrom = mailFrom; InetAddress localIP = InetAddress.getLocalHost(); this.localMachine = localIP.getHostAddress(); } catch (UnknownHostException ex) { Logger.getLogger(MailSender.class.getName()).log(Level.SEVERE, null, ex); } } //getter and setter public void smtp(String command) { try { System.out.println(sIn.readLine()); os.write((command + "\n").getBytes()); System.out.println("end"); } catch (IOException ex) { System.out.println(ex); Logger.getLogger(MailSender.class.getName()).log(Level.SEVERE, null, ex); } } BufferedReader sIn; OutputStream os; public Boolean trySend(String destUser, String subject, String message) { Socket socket; try { socket = new Socket(localSMTPServer, 587); OutputStreamWriter sOutW = new OutputStreamWriter(socket.getOutputStream(), "ASCII"); PrintWriter sOut = new PrintWriter(sOutW, true); InputStreamReader sInR = new InputStreamReader(socket.getInputStream(), "ASCII"); sIn = new BufferedReader(sInR); os = socket.getOutputStream(); this.nextSMTPCode(sIn, 220); sOut.println("EHLO"); this.nextSMTPCode(sIn, 250); sOut.println("AUTH LOGIN"); this.nextSMTPCode(sIn, 250); sOut.println("xxxxxxxxxxxxxxx=="); //username sOut.println("xxxxxxxxxxxxxx="); //password this.nextSMTPCode(sIn, 250); sOut.println("MAIL FROM: <" + this.mailFrom + ">"); this.nextSMTPCode(sIn, 250); sOut.println("RCPT TO: <" + destUser + ">"); this.nextSMTPCode(sIn, 250); sOut.println("DATA"); this.nextSMTPCode(sIn, 250); sOut.println("From: " + this.mailFrom); sOut.println("To: " + destUser); sOut.println("Subject: " + subject); sOut.println(message); sOut.println("."); this.nextSMTPCode(sIn, 250); sOut.println("QUIT"); this.nextSMTPCode(sIn, 250); sIn.close(); textMail = "From " + this.mailFrom + " To " + destUser + "\n" + "Subject: " + subject + "\nMessage: " + message; return true; } catch (IOException ex) { Logger.getLogger(MailSender.class.getName()).log(Level.SEVERE, null, ex); } return false; } public String getMail() { return textMail; } private void nextSMTPCode(BufferedReader sIn, int expectedCode) throws IOException { int code; String str = sIn.readLine(); System.out.println(str); String sCode = str.substring(0, 3); code = Integer.parseInt(sCode); if (code != expectedCode) { sIn.close(); System.out.println("Code: " + code + " expected code was: " + expectedCode); throw new IOException(); } } }
在与SMTP服务器(smtp2go)通信时遇到问题:即便在不该返回250码的场景下,也持续收到250响应码,最终导致邮件无法发送。
以下是一段通信示例:
220 mail.smtp2go.com ESMTP Exim 4.96-S2G Tue, 10 Jan 2023 14:42:02+0000 250-mail.smtp2go.com Hello [xxxxx] 250-SIZE 52428800 250-8BITMIME 250-DSN 250-PIPELINING 250-PIPECONNECT 250-AUTH CRAM-MD5 PLAIN LOGIN 250-CHUNKING
之后发送的每一条命令,都会收到250-SIZE 52428800等带250码的响应。用telnet测试一切正常,问题显然出在代码中,但无法定位错误。
解决方案
核心问题:未正确处理SMTP多线响应
SMTP服务器对EHLO等命令的响应是多线格式,每条行开头都是响应码(如250),但只有最后一行的响应码后不带连字符(-),表示响应结束。你的nextSMTPCode方法仅读取了第一行响应,剩余的多线内容会留在输入缓冲区中。后续发送命令后,程序读取的其实是之前EHLO响应的剩余行,而非新命令的真实响应,因此会持续收到250开头的内容。
修复步骤
完善响应读取逻辑
修改nextSMTPCode方法,循环读取所有响应行,直到找到不带连字符的目标响应码:private void nextSMTPCode(BufferedReader sIn, int expectedCode) throws IOException { int code; String str; do { str = sIn.readLine(); if (str == null) { throw new IOException("SMTP连接意外关闭"); } System.out.println(str); String sCode = str.substring(0, 3); code = Integer.parseInt(sCode); } while (str.length() > 3 && str.charAt(3) == '-'); if (code != expectedCode) { sIn.close(); System.out.println("收到响应码: " + code + " 预期响应码: " + expectedCode); throw new IOException(); } }修正命令的预期响应码
代码中多处预期响应码错误,这也是发送失败的关键:AUTH LOGIN命令的正确响应码是334(提示输入用户名),发送用户名后再次收到334(提示输入密码),密码验证成功后返回235。DATA命令的响应码是354(提示开始输入邮件内容),而非250。EHLO命令需要携带本地主机名,不能只发送EHLO,应改为sOut.println("EHLO " + this.localMachine);。QUIT命令的响应码是221,而非250。
修正后的
trySend方法相关片段:this.nextSMTPCode(sIn, 220); sOut.println("EHLO " + this.localMachine); this.nextSMTPCode(sIn, 250); sOut.println("AUTH LOGIN"); this.nextSMTPCode(sIn, 334); // 等待用户名提示 sOut.println("xxxxxxxxxxxxxxx=="); // 用户名Base64编码 this.nextSMTPCode(sIn, 334); // 等待密码提示 sOut.println("xxxxxxxxxxxxxx="); // 密码Base64编码 this.nextSMTPCode(sIn, 235); // 认证成功 sOut.println("MAIL FROM: <" + this.mailFrom + ">"); this.nextSMTPCode(sIn, 250); sOut.println("RCPT TO: <" + destUser + ">"); this.nextSMTPCode(sIn, 250); sOut.println("DATA"); this.nextSMTPCode(sIn, 354); // 等待输入内容 sOut.println("From: " + this.mailFrom); sOut.println("To: " + destUser); sOut.println("Subject: " + subject); sOut.println(); // 邮件头和正文之间需要空行 sOut.println(message); sOut.println("."); this.nextSMTPCode(sIn, 250); sOut.println("QUIT"); this.nextSMTPCode(sIn, 221);补充邮件格式细节
邮件头和正文之间必须有一个空行,否则服务器可能无法正确解析邮件内容,这也是潜在的发送失败原因。
内容的提问来源于stack exchange,提问作者Conta
相关产品推荐
相关产品推荐

