The reasons are grouped into bounce types. Following are the bounce types recognized by this scanner:
HARD_BOUNCE
SOFT_BOUNCE
MAILBOX_FULL
CC_USER
MDN_RECEIPT // read receipt
/*
* blog/javaclue/javamail/SmtpScanner.java
*
* Copyright (C) 2009 JackW
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with this library.
* If not, see <http://www.gnu.org/licenses/>.
*/
package blog.javaclue.javamail;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.log4j.Logger;
/**
* Scan input string for RFC1893/RFC2821 mail status code
*
* @author jackw
*/
final class SmtpScanner {
static final Logger logger = Logger.getLogger(SmtpScanner.class);
static final boolean isDebugEnabled = logger.isDebugEnabled();
final int maxLenToScan = 8192*4; // scan up to 32k
private static final HashMap<String, String> RFC1893_STATUS_CODE = new HashMap<String, String>();
private static final HashMap<String, String> RFC1893_STATUS_DESC = new HashMap<String, String>();
private static final HashMap<String, String> RFC2821_STATUS_CODE = new HashMap<String, String>();
private static final HashMap<String, String> RFC2821_STATUS_DESC = new HashMap<String, String>();
private static final HashMap<String, String> RFC2821_STATUS_MATCHINGTEXT = new HashMap<String, String>();
public static enum BOUNCETYPE { GENERIC }; // default bounce type - not a bounce.
public static enum BOUNCE_TYPES {
HARD_BOUNCE, // Hard bounce - suspend,notify,close
SOFT_BOUNCE, // Soft bounce - bounce++,close
MAILBOX_FULL, // mailbox full, can be treated as Soft Bounce
CC_USER, // Mail received as a Carbon Copy
MDN_RECEIPT, // MDN - read receipt
}
private static SmtpScanner smtpCodeScan = null;
private static final String
LETTER_S = "s",
LETTER_H = "h",
LETTER_F = "f",
LETTER_K = "k",
LETTER_U = "u";
/**
* default constructor
*/
private SmtpScanner() throws IOException {
loadRfc1893StatusCode();
loadRfc2821StatusCode();
}
public static SmtpScanner getInstance() throws IOException {
if (smtpCodeScan==null) {
smtpCodeScan = new SmtpScanner();
}
return smtpCodeScan;
}
/**
* returns a message id, null if not found
*
* @param str -
* message body
* @return message id, null if not found
*/
String scanBody(String body) {
String bounceType = scanBody(body, 1);
if (bounceType == null) {
bounceType = scanBody(body, 2);
}
return bounceType;
}
private static Pattern pattern1 = Pattern.compile("\\s([245]\\.\\d{1,3}\\.\\d{1,3})\\s", Pattern.DOTALL);
private static Pattern pattern2 = Pattern.compile("\\s([245]\\d\\d)\\s", Pattern.DOTALL);
/**
* <ul>
* <li> first pass: check if it contains a RFC1893 code. RFC1893 codes are
* from 5 to 9 bytes long (x.x.x -> x.xxx.xxx) and start with 2.x.x or 4.x.x
* or 5.x.x
* <li> second pass: check if it contains a 3 digit numeric number: 2xx, 4xx
* or 5xx.
* </ul>
*
* @param str -
* message body
* @param pass -
* 1) first pass: look for RFC1893 token (x.x.x).
* 2) second pass: look for RFC2821 token (xxx), must also match reply text.
* @return bounce type or null if no RFC code is found.
*/
private String scanBody(String body, int pass) {
if (isDebugEnabled)
logger.debug("Entering the examineBody method, pass " + pass);
if (StringUtil.isEmpty(body)) { // sanity check
return null;
}
BOUNCE_TYPES bounceType = null;
if (pass == 1) {
Matcher m = pattern1.matcher(StringUtil.cut(body, maxLenToScan));
if (m.find()) { // only one time
String token = m.group(m.groupCount());
logger.info("examineBody(): RFC1893 token found: " + token);
if ((bounceType = searchRfc1893CodeTable(token)) != null) {
return bounceType.toString();
}
else if (token.startsWith("5.")) { // 5.x.x
return BOUNCE_TYPES.HARD_BOUNCE.toString();
}
else if (token.startsWith("4.")) { // 4.x.x
return BOUNCE_TYPES.SOFT_BOUNCE.toString();
}
else if (token.startsWith("2.")) { // 2.x.x
// 2.x.x = OK message returned, MDN receipt.
return BOUNCE_TYPES.MDN_RECEIPT.toString();
}
}
}
else if (pass == 2) {
Matcher m = pattern2.matcher(StringUtil.cut(body, maxLenToScan));
int end = 0;
int count = 0;
while (m.find(end) && count++ < 2) { // repeat two times
String token = m.group(m.groupCount());
end = m.end(m.groupCount());
logger.info("examineBody(): Numeric token found: " + token);
if ((bounceType = searchRfc2821CodeTable(token)) != null) {
//return bounceType;
return matchRfcText(bounceType, token, body, end);
}
if (token.startsWith("5")) {
// 5xx = permanent failure, re-send will fail
String r = matchRfcText(BOUNCE_TYPES.HARD_BOUNCE, token, body, end);
if (r != null) return r;
// else look for the second token
}
else if(token.equals("422")) {
// 422 = mailbox full, re-send may be successful
return matchRfcText(BOUNCE_TYPES.MAILBOX_FULL, token, body, end);
}
else if (token.startsWith("4")) {
// 4xx = persistent transient failure, re-send may be successful
String r = matchRfcText(BOUNCE_TYPES.SOFT_BOUNCE, token, body, end);
if (r != null) return r;
// else look for the second token
}
else if(token.startsWith("2")) {
// 2xx = OK message returned.
}
}
}
return null;
}
/**
* For RFC 2821, to further match reply text to prevent false positives.
*
* @param bounceType -
* Bounce Type
* @param code -
* RFC2821 code
* @param tokens -
* message text stored in an array, each element holds a word.
* @param idx -
* where the RFC2821 code located in the array
* @return bounce type, or null if failed to match reply text.
*/
private String matchRfcText(BOUNCE_TYPES bounceType, String code, String body, int idx) {
String matchingText = RFC2821_STATUS_MATCHINGTEXT.get(code);
if (matchingText == null) {
if (code.startsWith("4")) {
matchingText = RFC2821_STATUS_MATCHINGTEXT.get("4xx");
}
else if (code.startsWith("5")) {
matchingText = RFC2821_STATUS_MATCHINGTEXT.get("5xx");
}
if (matchingText == null) { // just for safety
return null;
}
}
// RFC reply text - the first 120 characters after the RFC code
String rfcText = StringUtil.cut(body.substring(idx), 120);
try {
Pattern p = Pattern.compile(matchingText, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(rfcText);
if (m.find()) {
logger.info("Match Succeeded: [" + rfcText + "] matched [" + matchingText + "]");
return bounceType.toString();
}
else {
logger.info("Match Failed: [" + rfcText + "] did not match [" + matchingText + "]");
}
}
catch (PatternSyntaxException e) {
logger.error("PatternSyntaxException caught", e);
}
return null;
}
/**
* search smtp code table by RFC1893 token.
*
* @param token
* DSN status token, for example: 5.0.0
* @return message id related to the token
*/
private BOUNCE_TYPES searchRfc1893CodeTable(String token) {
// search rfc1893 hash table - x.x.x
BOUNCE_TYPES bounceType = searchRfcCodeTable(token, RFC1893_STATUS_CODE);
// search rfc1893 hash table - .x.x
if (bounceType == null) {
bounceType = searchRfcCodeTable(token.substring(1), RFC1893_STATUS_CODE);
}
return bounceType;
}
/**
* search smtp code table by RFC token.
*
* @param token -
* DSN status token, for example: 5.0.0, or 500 depending on the
* map used
* @param map -
* either RFC1893_STATUS_CODE or RFC2821_STATUS_CODE
* @return message id of the token
*/
private BOUNCE_TYPES searchRfcCodeTable(String token, HashMap<String, String> map) {
String type = map.get(token);
if (type != null) { // found RFC status code
logger.info("searchRfcCodeTable(): A match is found for type: " + type);
if (type.equals(LETTER_H)) {
return BOUNCE_TYPES.HARD_BOUNCE;
}
else if (type.equals(LETTER_S)) {
return BOUNCE_TYPES.SOFT_BOUNCE;
}
else if (type.equals(LETTER_F)) {
return BOUNCE_TYPES.MAILBOX_FULL;
}
else if (type.equals(LETTER_K)) {
return BOUNCE_TYPES.MDN_RECEIPT;
}
else if (type.equals(LETTER_U)) {
if (token.startsWith("4")) {
return BOUNCE_TYPES.SOFT_BOUNCE;
}
else if (token.startsWith("5")) {
return BOUNCE_TYPES.HARD_BOUNCE;
}
}
}
return null;
}
/**
* search smtp code table by RFC token.
*
* @param token -
* RFC2821 token, for example: 500
* @return message id of the token
*/
private BOUNCE_TYPES searchRfc2821CodeTable(String token) {
return searchRfcCodeTable(token, RFC2821_STATUS_CODE);
}
/**
* load the rfc1893 code table, from Rfc1893.properties file, into memory.
*
* @throws IOException
*/
private void loadRfc1893StatusCode() throws IOException {
ClassLoader loader = this.getClass().getClassLoader();
try {
// read in RFC1893 status code file and store it in two property objects
InputStream is = loader.getResourceAsStream("Rfc1893.properties");
BufferedReader fr = new BufferedReader(new InputStreamReader(is));
String inStr=null, code=null;
while ((inStr = fr.readLine()) != null) {
if (!inStr.startsWith("#")) {
if (isDebugEnabled)
logger.debug("loadRfc1893StatusCode(): " + inStr);
StringTokenizer st = new StringTokenizer(inStr, "^\r\n");
if (st.countTokens() == 3) {
code = st.nextToken();
RFC1893_STATUS_CODE.put(code, st.nextToken());
RFC1893_STATUS_DESC.put(code, st.nextToken());
}
else if (st.countTokens() == 0) {
// ignore
}
else {
logger.fatal("loadRfc1893StatusCode(): Wrong record format: " + inStr);
}
}
}
fr.close();
}
catch (FileNotFoundException ex) {
logger.fatal("file Rfc1893.properties does not exist", ex);
throw ex;
}
catch (IOException ex) {
logger.fatal("IOException caught during loading statcode.conf", ex);
throw ex;
}
}
/**
* load the rfc2821 code table, from Rfc2821.properties file, into memory.
*
* @throws IOException
*/
private void loadRfc2821StatusCode() throws IOException {
ClassLoader loader = this.getClass().getClassLoader();
try {
// read in RFC2821 status code file and store it in two property objects
InputStream is = loader.getResourceAsStream("Rfc2821.properties");
BufferedReader fr = new BufferedReader(new InputStreamReader(is));
String inStr=null, code=null;
while ((inStr = fr.readLine()) != null) {
if (!inStr.startsWith("#")) {
if (isDebugEnabled)
logger.debug("loadRfc2821StatusCode(): " + inStr);
StringTokenizer st = new StringTokenizer(inStr, "^\r\n");
if (st.countTokens() == 3) {
code = st.nextToken(); // 1st token = RFC code
RFC2821_STATUS_CODE.put(code, st.nextToken()); // 2nd token = type
String desc = st.nextToken(); // 3rd token = description
RFC2821_STATUS_DESC.put(code, desc);
// extract regular expression to be further matched
String matchingRegex = getMatchingRegex(desc);
if (matchingRegex != null) {
RFC2821_STATUS_MATCHINGTEXT.put(code, matchingRegex);
}
}
else if (st.countTokens() == 0) {
// ignore
}
else {
logger.fatal("loadRfc2821StatusCode(): Wrong record format: " + inStr);
}
}
}
fr.close();
}
catch (FileNotFoundException ex) {
logger.fatal("file Rfc2821.properties does not exist", ex);
throw ex;
}
catch (IOException ex) {
logger.fatal("IOException caught during loading statcode.conf", ex);
throw ex;
}
}
private String getMatchingRegex(String desc) throws IOException {
int left = desc.indexOf("{");
if (left < 0) {
return null;
}
Stack<Integer> stack = new Stack<Integer>();
stack.push(Integer.valueOf(left));
int nextPos = left;
while (stack.size() > 0) {
int leftPos = desc.indexOf("{", nextPos + 1);
int rightPos = desc.indexOf("}", nextPos + 1);
if (leftPos > rightPos) {
if (rightPos > 0) {
stack.pop();
nextPos = rightPos;
}
}
else if (leftPos < rightPos) {
if (leftPos > 0) {
nextPos = leftPos;
stack.push(Integer.valueOf(leftPos));
}
else if (rightPos > 0) {
stack.pop();
nextPos = rightPos;
}
}
else {
break;
}
}
if (nextPos > left) {
if (stack.size() == 0) {
return desc.substring(left + 1, nextPos);
}
else {
logger.error("getMatchingRegex() - missing close curly brace: " + desc);
throw new IOException("Missing close curly brace: " + desc);
}
}
return null;
}
public static void main(String[] args) {
try {
SmtpScanner scan = SmtpScanner.getInstance();
String bounceType = scan.scanBody("aaaaab\n5.0.0\nefg ");
System.out.println("BounceType: " + bounceType);
bounceType = scan.scanBody("aaa 201 aab\n422\naccount is full ");
System.out.println("BounceType: " + bounceType);
bounceType = scan.scanBody("aaaaab\n400\ntemporary failure ");
System.out.println("BounceType: " + bounceType);
System.out.println(scan.getMatchingRegex("{(?:mailbox|account).{0,180}(?:storage|full|limit|quota)}"));
}
catch (Exception e) {
e.printStackTrace();
}
}
}
This class needs two additional property files to function, save them under the root folder of your classpath:
1) Rfc1893.properties:
# RFC1893/RFC3463 status code and description # status code = class "." subject "." detail # 2.x.x Success # 4.x.x Persistent Transient Failure # 5.x.x Permanent Failure # # format: StatusCode^Type^Description # type =# # permanent failure 5.0.0^h^Other undefined status 5.1.0^h^Other address status 5.1.1^h^Bad destination mailbox address 5.1.2^h^Bad destination system address 5.1.3^h^Bad destination mailbox address syntax 5.1.4^h^Destination mailbox address ambiguous 5.1.5^h^Destination mailbox address invalid (source: Microsoft) 5.1.6^h^Mailbox has moved 5.1.7^h^Bad sender's mailbox address syntax 5.1.8^h^Bad sender's system address 5.2.0^h^Other or undefined mailbox status 5.2.1^h^Mailbox disabled, not accepting messages 5.2.2^f^Mailbox full 5.2.3^l^Message length exceeds administrative limit. 5.2.4^h^Mailing list expansion problem 5.3.0^h^Other or undefined mail system status 5.3.1^s^Mail system full 5.3.2^h^System not accepting network messages 5.3.3^h^System not capable of selected features 5.3.4^l^Message too big for system 5.3.5^h^System incorrectly configured 5.4.0^h^Other or undefined network or routing status 5.4.1^h^No answer from host 5.4.2^h^Bad connection 5.4.3^h^Routing server failure 5.4.4^h^Unable to route 5.4.5^h^Network congestion 5.4.6^h^Routing loop detected 5.4.7^h^Delivery time expired 5.4.8^h^Loop detected, check recipient policy. (Source: Microsoft) 5.5.0^h^Other or undefined protocol status 5.5.1^h^Invalid command 5.5.2^h^Syntax error 5.5.3^h^Too many recipients 5.5.4^h^Invalid command arguments 5.5.5^h^Wrong protocol version 5.6.0^b^Other or undefined media error 5.6.1^b^Media not supported 5.6.2^h^Conversion required and prohibited 5.6.3^h^Conversion required but not supported 5.6.4^h^Conversion with loss performed 5.6.5^h^Conversion failed 5.7.0^h^Other or undefined security status 5.7.1^b^Delivery not authorized, message refused 5.7.2^h^Mailing list expansion prohibited 5.7.3^h^Security conversion required but not possible 5.7.4^h^Security features not supported 5.7.5^b^Cryptographic failure 5.7.6^b^Cryptographic algorithm not supported 5.7.7^b^Message integrity failure # persistent transient failure 4.0.0^s^Other undefined status 4.1.0^s^Other address status 4.1.4^s^Destination mailbox address ambiguous 4.1.5^s^Destination mailbox address valid 4.1.7^s^Bad sender's mailbox address syntax 4.1.8^s^Bad sender's system address 4.2.0^s^Other or undefined mailbox status 4.2.1^s^Mailbox disabled, not accepting messages 4.2.2^f^Mailbox full 4.2.4^s^Mailing list expansion problem 4.3.0^s^Other or undefined mail system status 4.3.1^s^Mail system full 4.3.2^s^System not accepting network messages 4.3.3^s^System not capable of selected features 4.3.5^s^System incorrectly configured 4.4.0^s^Other or undefined network or routing status 4.4.1^s^No answer from host 4.4.2^s^Bad connection 4.4.3^s^Routing server failure 4.4.4^s^Unable to route 4.4.5^s^Network congestion 4.4.6^s^Routing loop detected 4.4.7^s^Delivery time expired 4.5.0^s^Other or undefined protocol status 4.5.3^s^Too many recipients 4.5.5^s^Wrong protocol version 4.6.0^s^Other or undefined media error 4.6.2^s^Conversion required and prohibited 4.6.3^s^Conversion required but not supported 4.6.4^s^Conversion with loss performed 4.6.5^s^Conversion failed 4.7.0^s^Other or undefined security status 4.7.5^s^Cryptographic failure 4.7.6^s^Cryptographic algorithm not supported 4.7.7^s^Message integrity failure # generic entries .0.0^s^Other undefined status .1.0^s^Other address status .1.1^h^Bad destination mailbox address .1.2^h^Bad destination system address .1.3^h^Bad destination mailbox address syntax .1.4^h^Destination mailbox address ambiguous .1.5^k^Destination mailbox address valid .1.6^h^Mailbox has moved .1.7^s^Bad sender's mailbox address syntax .1.8^s^Bad sender's system address .2.0^s^Other or undefined mailbox status .2.1^h^Mailbox disabled, not accepting messages .2.2^f^Mailbox full .2.3^l^Message length exceeds administrative limit. .2.4^s^Mailing list expansion problem .3.0^s^Other or undefined mail system status .3.1^s^Mail system full .3.2^s^System not accepting network messages .3.3^s^System not capable of selected features .3.4^l^Message too big for system .3.5^s^System incorrectly configured .4.0^s^Other or undefined network or routing status .4.1^s^No answer from host .4.2^s^Bad connection .4.3^s^Routing server failure .4.4^s^Unable to route .4.5^s^Network congestion .4.6^s^Routing loop detected .4.7^s^Delivery time expired .5.0^s^Other or undefined protocol status .5.1^h^Invalid command .5.2^h^Syntax error .5.3^s^Too many recipients .5.4^h^Invalid command arguments .5.5^s^Wrong protocol version .6.0^s^Other or undefined media error .6.1^s^Media not supported .6.2^s^Conversion required and prohibited .6.3^s^Conversion required but not supported .6.4^s^Conversion with loss performed .6.5^s^Conversion failed .7.0^s^Other or undefined security status .7.1^b^Delivery not authorized, message refused .7.2^h^Mailing list expansion prohibited .7.3^h^Security conversion required but not possible .7.4^h^Security features not supported .7.5^s^Cryptographic failure .7.6^s^Cryptographic algorithm not supported .7.7^s^Message integrity failure
2) Rfc2821.properties:
# RFC2821 reply code and description # reply code = xyz # 1yz Positive Preliminary reply # 2yz Positive Completion reply # 3yz Positive Intermediate reply # 4yz Transient Negative Completion reply # 5yz Permanent Negative Completion reply # # format: ReplyCode^Type^Description # type =# Description: text enclosed in curly brackets should be further matched to prevent false positives. # 211^k^System status, or system help reply 214^k^Help message 220^k^Service ready 221^k^ Service closing transmission channel 250^k^Requested mail action okay, completed 251^k^User not local; will forward to 252^k^Cannot VRFY user, but will accept message and attempt delivery 354^k^Start mail input; end with . 421^s^ Service not available, closing transmission channel {\bnot\s+available} 450^s^Requested mail action not taken: mailbox unavailable {\baction\s+not\s+taken} 451^s^Requested action aborted: local error in processing {\baction\s+aborted} 452^s^Requested action not taken: insufficient system storage {\baction\s+not\s+taken} 500^h^Syntax error, command unrecognized {\berror} 501^h^Syntax error in parameters or arguments {\berror} 502^h^Command not implemented {\bnot\s+implemented} 503^h^Bad sequence of commands {\bBad\s+sequence} 504^h^Command parameter not implemented {\bnot\s+implemented} 550^h^Requested action not taken: mailbox unavailable {\baction\s+not\s+taken} 551^h^User not local; please try {\bnot\s+local} 552^f^Requested mail action aborted: exceeded storage allocation {\baction\s+aborted} 553^h^Requested action not taken: mailbox name not allowed {\baction\s+not\s+taken} 554^h^Transaction failed {\b(?:failed|delivery error)} # # *** Custom entries, not defined by RFC 2821 *** # 422^f^{\b(?:mailbox|account)\b.{0,100}(?:storage|full|limit|quota)} mailbox full. 4xx^s^{\btemporary\s.{0,100}(?:failure|error)}, used to match undefined codes starting with 4 5xx^h^{\bpermanent\s.{0,100}(?:failure|error)}, used to match undefined codes starting with 5
Thank you very much for your good code. Can you please tell me what is the StringUtil class you used here. I didn't see it in imports.
ReplyDeleteThank you,
Vissu
Please ignore my above one... I didn't see your old post.
DeleteNow, It is clear here... http://javaclue.blogspot.in/2009/09/portable-java-mail-message-bean-part-5.html.