
/* *****************************************************************************
 *		ITSV GmbH
 *		SVCLEAR - SV-Clearingsystem Koordination
 *		PMTOOLS
 *		MODULE:			ReceiveMail
 *		HISTORY:
 *
 *
 */
 
import java.io.IOException;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Properties;
import java.util.Date;
import java.util.Calendar;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Enumeration;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

import javax.mail.*;

/*
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
*/

import javax.mail.search.*;

import javax.mail.internet.MimeBodyPart;

import com.sun.mail.pop3.POP3Store;
import com.sun.mail.imap.YoungerTerm;

import org.apache.commons.csv.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/*
import org.json.simple.JsonArray;
import org.json.simple.JsonObject;
import org.json.simple.parser.JSONParser;
*/

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;

/* *****************************************************************************
 *	CLASS:			ReceiveMail
 *	DESCRIPTION:	JavaMail usage inspired by https://www.rgagnon.com/javadetails/java-receive-email-using-imap.html
 */

public class ReceiveMail {
	private static final Logger logger = LoggerFactory.getLogger(ReceiveMail.class);
	
	public static final int C_ERROR_INVALID_BASEPATH		=	   998;
		
	private static final int CBUFSIZE = 1000000;
	
	private static int sequenceNumber = 0;
	private static String lastUniqueDatnum = new String("DEADBEEF00");
	private static HashMap<String,String> knownIds = null;
	
	private HashMap<String,String> SVTnameLookup;
	
	private static final SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
	
	private static final SimpleDateFormat datnumformat = new SimpleDateFormat("yyyyMMddhhmmssSSS");
	
	public ReceiveMail() {
		logger.debug("ReceiveMail.default_constructor");
		SVTnameLookup = new HashMap<String,String>();
		SVTnameLookup.put("11","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sWien\\s*$");
		SVTnameLookup.put("12","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sNieder.*sterreich\\s*$");
		SVTnameLookup.put("13","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sBurgenland\\s*$");
		SVTnameLookup.put("14","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sOber.*sterreich\\s*$");
		SVTnameLookup.put("15","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sSteiermark\\s*$");
		SVTnameLookup.put("16","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sK.*rnten\\s*$");
		SVTnameLookup.put("17","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sSalzburg\\s*$");
		SVTnameLookup.put("18","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sTirol\\s*$");
		SVTnameLookup.put("19","^\\s*.sterreichische\\s+Gesundheitsk.+se\\sVorarlberg\\s*$");
		SVTnameLookup.put("05","^\\s*BVAEB.*Eisenbahn.*Bergbau\\s*$");
		logger.debug("ReceiveMail.SVTnameLookup_initialized");
	}
	
	public static String escapeJSONString(String s) {
		if (s==null) return null;
		return s.replaceAll("\"","\\\"");
	}
		
	public static String dateToString(Date d) {
		return dateformat.format(d);
	}
	
	public static String dateToDatnum(Date d) {
		return (d==null?"<null>":datnumformat.format(d));
	}
	
	/* *************************************************************************
	 *		METHOD:			uniqueIdFromDate
	 *		INPUT:			d		- a Date to be used as base reference for the ID
	 *		RESULT:			a 8- or 14-character unique ID 
	 *		DESCRIPTION:	creates a new unique ID, based on date <d>
	 *						formatted like YYYYMMDDqqqqqq
	 *						qqqqqq ... 6-digit sequence number unique to the date
	 *									is appended if YYYYMMDD is not unique
	 *						if <d> is null, the current day is used
	 *						
	 */
	public static String uniqueIdFromDate(Date d) {
		if (knownIds==null) knownIds = new HashMap<String,String>();
		String datnum = dateToDatnum(d==null?(new Date()):d);
		String newid = new String(datnum);
		while (knownIds.get(newid)!=null) {
			if (datnum.equals(lastUniqueDatnum)) {
				sequenceNumber++;
			} else {
				sequenceNumber = 0;
				lastUniqueDatnum = datnum;
			}
			StringBuilder sb = new StringBuilder();
			sb.append(lastUniqueDatnum);
			sb.append(String.format("%06d",sequenceNumber));
			newid = sb.toString();
		}
		knownIds.put(newid,newid);
		return newid;
	}
	
	/* *************************************************************************
	 *		METHOD:			hexDumpStringLog
	 *		DESCRIPTION:	log contents of <data> as hex dump
	 *						- 16 characters per line
	 *						- 1 column with 4-digit hex values
	 *						- 1 column as printable characters (or '.' if not prontable)
	 *						each line is prefixed with <prefix> and 8-digit hex relative position
	 *						of the first character in the line
	 */
	public static void hexDumpStringLog(String data, String prefix) {
		if (data==null) {
			logger.debug(String.format("%s: <null>",prefix));
			return;
		}
		int pos = 0;
		int ci;
		int cc;
		StringBuilder hsb, asb;
		while (pos<data.length()) {
			hsb = new StringBuilder();
			asb = new StringBuilder();
			hsb.append(String.format("%08X : ",pos));
			for (ci = 0; ci<16; ci++) {
				if (pos<data.length()) {
					cc = Character.codePointAt(data,pos);
					hsb.append(String.format("%04X ",cc));
					if ((cc>=32) && (cc<=126)) {
						asb.append(String.format("%c",(char)cc));
					} else {
						asb.append(".");
					}
				} else {
					hsb.append("     ");
					asb.append(" ");
				}
				pos++;
			}
			logger.debug(String.format("%s.%s |%s|",prefix,hsb.toString(),asb.toString()));
		}
	}

	//
	// match a string against a regular expression
	//
	public static boolean matchString(String target, String regex) {
		boolean r = false;
		Pattern pat = Pattern.compile(regex);
		Matcher mat;
		// logger.debug(String.format("matchString.regex=\"%s\".pattern=\"%s\".flags=%d",pat.pattern(),pat.toString(),pat.flags()));
		if (target!=null) {
			mat = pat.matcher(target);
			// logger.debug(String.format("matchString.matcher.pattern=\"%s\"",mat.pattern()));
			r = mat.matches();
		} else {
			r = false;
		}
		// logger.debug(String.format("matchString: STRING=\"%s\" REGEX=\"%s\" MATCH=%s",(target==null?"<null>":(target.length()>80?target.substring(0,80):target)),regex,r));
		return r;
	}

	//
	// match and capture one item from a string against a regular expression
	//
	public static String captureString(String target, String regex) {
		String res;
		boolean r = false;
		if (target==null) {
			res = null;
		} else {
			Pattern pat = Pattern.compile(regex);
			Matcher mat;
			mat = pat.matcher(target);
			if (r = mat.matches()) {
				res =  mat.group(1);
			} else {
				res = null;
			}
		}
		logger.debug(String.format("captureString: STRING=\"%s\" REGEX=\"%s\" MATCH=%s C=\"%s\"",(target==null?"<null>":(target.length()>80?target.substring(0,80):target)),regex,r,((res!=null)?res:"<null>")));
		return res;
	}
	
	//
	// find a match from a list of possible regexes
	//
	public static String lookupRegexList(String target, HashMap<String,String> lookup) {
		for (String name : lookup.keySet()) {
			String regex = lookup.get(name);
			if (matchString(target,regex)) return name;
		}
		return null;
	}
	
	private class AddressList {
		private ArrayList<String> _adrlist = null;
		
		public AddressList() {
			_adrlist = new ArrayList<String>();
		}
		
		public void add(String adr) {
			if (_adrlist==null) {
				_adrlist = new ArrayList<String>();
			}
			_adrlist.add(adr);
		}
		
		public void addAddresses(Address[] ad) {
			if (_adrlist==null) {
				_adrlist = new ArrayList<String>();
			}
			if (ad==null) return;
			for (int ai=0; ai<ad.length; ai++) {
				_adrlist.add(ad[ai].toString());
			}
		}
			
		public String get(int index) {
			if (_adrlist==null) return null;
			if (index>_adrlist.size()) return null;
			return _adrlist.get(index);
		}
		
		public int size() {
			if (_adrlist==null) return 0;
			return _adrlist.size();
		}
		
		public String toAddressString() {
			StringBuilder sb = new StringBuilder();
			for (int i=0; i<_adrlist.size(); i++) {
				if (i>0) sb.append(";");
				sb.append(_adrlist.get(i));
			}
			return sb.toString();
		}
		
		public JsonArray toJsonArray() {
			JsonArray ja = new JsonArray();
			for (int i=0; i<_adrlist.size(); i++) {
				ja.add(_adrlist.get(i));
			}
			return ja;
		}
		
	}
	
	private class PartList {
		private ArrayList<AMessage> _partlist = null;
		
		public PartList() {
			_partlist = new ArrayList<AMessage>();
		}
		
		public void add(AMessage msg) {
			if (_partlist==null) {
				_partlist = new ArrayList<AMessage>();
			}
			_partlist.add(msg);
		}
		
		public AMessage get(int index) {
			if (_partlist==null) return null;
			if (index>_partlist.size()) return null;
			return _partlist.get(index);
		}
		
		public int size() {
			if (_partlist==null) return 0;
			return _partlist.size();
		}
		
		public JsonArray toJsonArray() {
			JsonArray ja = new JsonArray();
			for (int i = 0; i < _partlist.size(); i++) {
				ja.add(_partlist.get(i).toJsonObject());
			}
			return ja;
		}
		
	}
	
	private class TextList {
		
		private ArrayList<String> _tlist = null;
				
		public void add(String ntxt) {
			if (_tlist==null) {
				_tlist = new ArrayList<String>();
			}
			_tlist.add(ntxt);
		}
		
		public String get(int index) {
			if (_tlist==null) return null;
			if (index>_tlist.size()) return null;
			return _tlist.get(index);
		}
		
		public int size() {
			if (_tlist==null) return 0;
			return _tlist.size();
		}
		
		public JsonArray toJsonArray() {
			JsonArray ja = new JsonArray();
			for (int i = 0; i < _tlist.size(); i++) {
				ja.add(_tlist.get(i));
			}
			return ja;
		}
	
	// end of class TextList
	}
	
	private class MatchPattern {
		
		private String pattern = "";
		private String info = "";
		
		public MatchPattern(String pat, String inf) {
			pattern = pat;
			info = inf;
		}
		
		public boolean matches(AMessage msg) {
			return msg.matchText(pattern);
		}
		
		public String getInfo() {
			return info;
		}
		
	}
	
	/* 
	 * a list of pattern that can be matched and return the associated info upon match
	 */
	private class PatternMatchList {
		
		private ArrayList<MatchPattern> _plist = null;
		
		public PatternMatchList(String [][] initializer) {
			for (int i = 0; i<initializer.length; i++) {
				add(initializer[i][0],initializer[i][1]);
			}
		}
		
		public void add(String pat, String inf) {
			MatchPattern newmp = new MatchPattern(pat,inf);
			if (_plist==null) {
				_plist = new ArrayList<MatchPattern>();
			}
			_plist.add(newmp);			
		}
		
		public String detect(AMessage msg) {
			if (_plist==null) return null;
			int i = 0;
			while (true) {
				if (i>=_plist.size()) return null;
				MatchPattern cp = _plist.get(i);
				if (cp.matches(msg)) return  cp.getInfo();
				i++;
			}
		}
	// end of subclass PatternMatchList
	}
	
	/*
	
	private void uploadManagedFile() {		
		String url = "http://example.com/upload";
		String charset = "UTF-8";
		String param = "value";
		File textFile = new File("/path/to/file.txt");
		File binaryFile = new File("/path/to/file.bin");
		String boundary = Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
		String CRLF = "\r\n"; // Line separator required by multipart/form-data.

		URLConnection connection = new URL(url).openConnection();
		connection.setDoOutput(true);
		connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

		try (
			OutputStream output = connection.getOutputStream();
			PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);
		) {
			// Send normal param.
			writer.append("--" + boundary).append(CRLF);
			writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
			writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
			writer.append(CRLF).append(param).append(CRLF).flush();

			// Send text file.
			writer.append("--" + boundary).append(CRLF);
			writer.append("Content-Disposition: form-data; name=\"textFile\"; filename=\"" + textFile.getName() + "\"").append(CRLF);
			writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF); // Text file itself must be saved in this charset!
			writer.append(CRLF).flush();
			Files.copy(textFile.toPath(), output);
			output.flush(); // Important before continuing with writer!
			writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.

			// Send binary file.
			writer.append("--" + boundary).append(CRLF);
			writer.append("Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + binaryFile.getName() + "\"").append(CRLF);
			writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(binaryFile.getName())).append(CRLF);
			writer.append("Content-Transfer-Encoding: binary").append(CRLF);
			writer.append(CRLF).flush();
			Files.copy(binaryFile.toPath(), output);
			output.flush(); // Important before continuing with writer!
			writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.

			// End of multipart/form-data.
			writer.append("--" + boundary + "--").append(CRLF).flush();
		}

		// Request is lazily fired whenever you need to obtain information about response.
		int responseCode = ((HttpURLConnection) connection).getResponseCode();
		System.out.println(responseCode); // Should be 200
	}
	
	*/
	
	/*
	 * AMessage represents one message with its attributes and parts, prepared to be analyzed
	 *
	 * JSON functionality: https://github.com/fangyidong/json-simple
	 *
	 */
	private class AMessage {
		public AddressList from = null;
		public AddressList to = null;
		public Date rxdate = null;
		public Date txdate = null;
		public String subject = null;
		public String text = null;
		public PartList parts = null;
		public TextList texts = null;
		public TextList headerlines = null;
		public HashMap<String,String> attributes = null;
		
		public AMessage() {
			from = new AddressList();
			to = new AddressList();
			rxdate = null;
			txdate = null;
			subject = null;
			text = null;
			parts = null;
			texts = null;
			attributes = new HashMap<String,String>();
		}
		
		public JsonObject attributesToJsonObject() {
			JsonObject jo = new JsonObject();
			for (Map.Entry<String,String> entry : attributes.entrySet()) {
				jo.addProperty(entry.getKey(),entry.getValue());
			}
			return jo;
		}
	
		public void listAttributes(String prefix) {
			logger.debug(String.format("%s.rxdate=%s",prefix,dateToDatnum(rxdate)));
			logger.debug(String.format("%s.txdate=%s",prefix,dateToDatnum(txdate)));
			logger.debug(String.format("%s.subject=%s",prefix,subject));
			for (Map.Entry<String,String> entry : attributes.entrySet()) {
				logger.debug(String.format("%s.%s=\"%s\"",prefix,entry.getKey(),entry.getValue()));
			}			
		}			

		public void populatecontentstring(String cstr) {
			if (texts==null) texts = new TextList();
			texts.add(cstr.replace("\r","").replace("\n"," "));
		}
		
		public void populatecontentstream(InputStream istr) {
			int c;
			StringBuilder sb = new StringBuilder();
			try {
				while ((c = istr.read()) != -1)
					sb.append(Character.toString((char)c));
			} catch (IOException e) {
				System.out.println("listcontentstream: Exception: "+e);
			}
			this.populatecontentstring(sb.toString());
		}

		public void populatecontentmulti(Multipart cmulti) throws MessagingException,IOException {
			int partnum = cmulti.getCount();
			for (int j=0; j < partnum; ++j) {
				MimeBodyPart part = (MimeBodyPart)cmulti.getBodyPart(j);
				this.populatecontent(part.getContent());
			}
		}

		public void populatesubmessage(Message message) throws MessagingException,IOException {
			if (parts==null) parts = new PartList();
			AMessage amsg = new AMessage();
			amsg.populateMessage(message);
			parts.add(amsg);
		}
	
		public void populatecontent(Object content) throws MessagingException,IOException {
			String msgtext;
			Class c;
			c = content.getClass();
			String classname = c.getCanonicalName();
			switch (classname) {
				case "javax.mail.internet.MimeMultipart":	this.populatecontentmulti((Multipart)content);
															break;
				case "java.lang.String":					this.populatecontentstring((String)content);
															break;
				case "com.sun.mail.imap.IMAPInputStream":	this.populatecontentstream((InputStream)content);
															break;
				case "com.sun.mail.imap.IMAPNestedMessage":	this.populatesubmessage((Message)content);
															break;
				default:									this.populatecontentstring(content.toString());
			}	
		}
		
		public void populateMessage(Message message) throws MessagingException,IOException {
			this.subject = message.getSubject();
			this.from.addAddresses(message.getFrom());
			this.to.addAddresses(message.getAllRecipients());
			this.rxdate = message.getReceivedDate();
			this.txdate = message.getSentDate();
			this.populatecontent(message.getContent());
			this.attributes.put("disposition",message.getDisposition());
			this.attributes.put("contentType",message.getContentType());
			/*
			Enumeration<String> allHeaderLines = message.getAllHeaderLines();
			String headerline;
			while (allHeaderLines.hasMoreElements()) {
				headerline = allHeaderLines.nextElement();
				if (this.headerlines==null) this.headerlines = new TextList();
				this.headerlines.add(headerline);
			}
			*/
			this.attributes.put("contentDescription",message.getDescription());
			/* removed: only supported by IMAPMessage
			this.attributes.put("contentTransferEncoding",message.getEncoding());
			*/
		}
		
		public JsonObject toJsonObject() {
			JsonObject jo = new JsonObject();
			if (from!=null) jo.add("from",from.toJsonArray());
			if (to!=null) jo.add("to",to.toJsonArray());
			jo.addProperty("rxdate",dateToDatnum(rxdate));
			jo.addProperty("txdate",dateToDatnum(txdate));
			jo.addProperty("subject",escapeJSONString(subject));
			jo.addProperty("text",text);
			if (parts!=null) jo.add("parts",parts.toJsonArray());
			if (texts!=null) jo.add("texts",texts.toJsonArray());
			if (headerlines!=null) jo.add("headerlines",headerlines.toJsonArray());
			if (attributes!=null) jo.add("attributes",attributesToJsonObject());
			return jo;
		}
		
		public String toJSONString() {
			String js = new Gson().toJson(toJsonObject());
			if (js.length()>65535) {
				logger.error(String.format("AMessage.toJSONString.length_of_JSON>65535=%d",js.length()));
			}
			return js;
		}			
		
		/* *********************************************************************
		 *		message classification
		 */
		 
		//
		// match subject against regular expression
		//
		public boolean matchSubject(String regex) {
			return matchString(this.subject,regex);
		}
		
		//
		// match and capture subject against regular expression
		//
		public String captureSubject(String regex) {
			return captureString(this.subject,regex);
		}
		
		//
		// match message text(s) against regular expression
		//
		public boolean matchText(String regex) {
			boolean r;
			if (matchString(this.text,regex)) return true;
			if (this.texts==null) return false;
			for (int i=0; i<this.texts.size(); i++) {
				if (matchString(this.texts.get(i),regex)) return true;
			}
			return false;
		}

		//
		// match and capture message text(s) against regular expression
		//
		public String captureText(String regex) {
			String r;
			if ((r = captureString(this.text,regex))!=null) return r;
			if (this.texts==null) return null;
			for (int i=0; i<this.texts.size(); i++) {
				if ((r = captureString(this.texts.get(i),regex))!=null) return r;
			}
			return null;
		}
		 
		//
		// check message if it (or one of its parts) has the CF-notification/urgence message in it
		//
		public boolean identifyCFnotification(AMessage holder, String prefix) {
			String bknrpart,bknr,svtnamepart,notificationreceiver;
			String[] bknrs = null,svtnames = null, svts = null;
			int i;
			if (holder==null) holder = this;
			boolean neueCF = this.matchSubject("^Neue Clearingf.*lle eingelangt.*$");
			boolean urgenzCF = this.matchSubject("^Urgenz Clearingf.*lle.*$");
			if (neueCF || urgenzCF) {
				if (this.to!=null && (this.to.size()>0)) {
					notificationreceiver = this.to.toAddressString();
				} else {
					notificationreceiver = null;
				}
				bknrpart = this.captureText("^.*Betroffene Beitragskontonummer.+?([0-9]{4,11}.*)$");
				hexDumpStringLog(bknrpart,prefix+".bknrpart");
				if (bknrpart!=null) {
					bknrpart = bknrpart.replace("\r\n","\n").replace("\n\r","\n").replace("\r","\n").replace("\n"," ");
					bknrs = bknrpart.split(" ");
					for (i=0; i<bknrs.length; i++) {
						bknrs[i] = captureString(bknrs[i],"^\\s*([0-9]{4,11}).*$");
					}
				}
				svtnamepart = this.captureText("^.*Bei fachlichen Fragen wenden Sie sich bitte an die\\s(.+?)\\..*$");
				hexDumpStringLog(svtnamepart,prefix+".svtnamepart");
				if (svtnamepart!=null) {
					svtnames = svtnamepart.split(",");
					svts = new String[svtnames.length];
					for (i=0; i<svtnames.length; i++) {
						svts[i] = lookupRegexList(svtnames[i],SVTnameLookup);
					}
				}
				if ((bknrpart!=null) && (svtnamepart!=null)) {
					holder.attributes.put("CF_NOTIF_TYP",(neueCF?"CF_EINGELANGT":(urgenzCF?"CF_URGENZ":"UNBEKANNT")));
					if (svts!=null) {
						for (i=0; i<svts.length; i++) {
							holder.attributes.put(String.format("CF_ZU_SVT_%02d",i),svts[i]);
						}
					}
					if (bknrs!=null) {
						for (i=0; i<bknrs.length; i++) {
							holder.attributes.put(String.format("CF_ZU_BKNR_%02d",i),bknrs[i]);
						}
					}
					if (svtnames!=null) {
						for (i=0; i<svtnames.length; i++) {
							holder.attributes.put(String.format("CF_ZU_SVTNAME_%02d",i),svtnames[i]);
						}
					}
					holder.attributes.put("CF_EMPF",notificationreceiver);
					holder.attributes.put("CF_GESENDET",dateToString(holder.txdate));
					holder.attributes.put("CF_EMPFANGEN",dateToString(holder.rxdate));
					return true;
				}
			}
			if (parts==null) return false;
			for (i = 0; i < this.parts.size(); i++) {
				if (this.parts.get(i).identifyCFnotification(holder,String.format("%s.SUB%02d",prefix,i))) return true;
			}
			return false;
		}
		
		//
		// check if the message if it (or one of its parts) signifies the receiver of the CFnotification as unreachable
		//
		public boolean identifyCFreceiverUnreachable(AMessage holder, String prefix) {
			boolean deliveryStatus = this.matchSubject("^Delivery Status Notification.*$");
			String failedEmailAddress;
			boolean unreachable = false;
			String unrcause;
			if (deliveryStatus) {
				if ((failedEmailAddress = this.captureText("^.*The following message to\\s(.*?)\\swas undeliverable.*$"))!=null) {
					holder.attributes.put("BOUNCE_CLASSIFICATION","UNREACHABLE");
					holder.attributes.put("UNREACH_EMAIL",failedEmailAddress);
					holder.attributes.put("UNREACH_CAUSE","StatusNotificationUndeliverable");
					unreachable = true;
				}
			}
			if (!unreachable) {
				unrcause = new String("no known reason for unreachability");
				String unreachMatch;
				PatternMatchList unreachMatchList = new PatternMatchList( new String[][]
					{{ "^.*RecipientNotFound.*$",            "RecipientNotFound"},
					 { "^.*could\\snot\\sbe\\sdelivered.*$", "could not be delivered" },
					 { "^.*essage.*undeliverable.*$",		 "message undeliverable"  },
					 { "^.*Delivery\\shas\\sfailed.*$",		 "Delivery has failed"    }
					});
				unreachMatch = unreachMatchList.detect(this);
				if (unreachMatch!=null) {
					unreachable = true;
					unrcause = unreachMatch;
				}
				
				/*
				if (!unreachable) {
					unreachable = this.matchText("^.*RecipientNotFound.*$");
					if (unreachable) unrcause = new String("RecipientNotFound");
				}
				if (!unreachable) {
					unreachable = this.matchText("^.*could\\snot\\sbe\\sdelivered.*$");
					if (unreachable) unrcause = new String("could not be delivered");
				}
				if (!unreachable) {
					unreachable = this.matchText("^.*essage.*undeliverable.*$");
					if (unreachable) unrcause = new String("message undeliverable");
				}
				if (!unreachable) {
					unreachable = this.matchText("^.*Delivery\\shas\\sfailed.*$");
					if (unreachable) unrcause = new String("Delivery has failed");
				}
				*/
				
				if (unreachable) {
					if ((failedEmailAddress = this.captureText("^.*Original\\-Recipient\\:\\srfc822\\;(.*?)\\s.*$"))==null) 
						failedEmailAddress = new String("<<unknown>>"); 
					holder.attributes.put("BOUNCE_CLASSIFICATION","UNREACHABLE");
					holder.attributes.put("UNREACH_EMAIL",failedEmailAddress);
					holder.attributes.put("UNREACH_CAUSE",unrcause);
					unreachable = true;
				}
			}
			if (unreachable) return true;
			if (parts==null) return false;
			for (int i = 0; i < this.parts.size(); i++) {
				if (parts.get(i).identifyCFreceiverUnreachable(holder,String.format("%s.SUB%02d",prefix,i))) return true;
			}
			return false;
		}
		
		public boolean identifyCFreceiverAbsent(AMessage holder, String prefix) {
			boolean absent = this.matchSubject("^.*Abwesend:.*$");
			if (!absent) absent = this.matchSubject("^.*ch\\*bin.*nicht\\serreichbar.*$");
			if (absent) {
				holder.attributes.put("BOUNCE_CLASSIFICATION","ABSENT");
				return true;
			}
			if (parts==null) return false;
			for (int i = 0; i < this.parts.size(); i++) {
				if (this.parts.get(i).identifyCFreceiverAbsent(holder,String.format("%s.SUB%02d",prefix,i))) return true;
			}
			return false;
		}
		
		public boolean identifyCFreceiverMailboxFull(AMessage holder, String prefix) {
			boolean boxfull = this.matchSubject("^.*Das\\sPostfach\\sdes\\sEmpf.+ngers\\sist\\svoll.*$");
			if (boxfull) {
				holder.attributes.put("BOUNCE_CLASSIFICATION","RXBOXFULL");
				return true;
			}
			if (parts==null) return false;
			for (int i = 0; i < this.parts.size(); i++) {
				if (this.parts.get(i).identifyCFreceiverMailboxFull(holder,String.format("%s.SUB%02d",prefix,i))) return true;
			}
			return false;
		}

		/*
		 * classify the message and fill the classification markup
		 */
		public void classify(String prefix) {
			for (int i = 0; i < this.from.size(); i++) {
				this.attributes.put(String.format("CLS_FROM_%02d",i),this.from.get(i));
			}
			for (int i = 0; i < this.to.size(); i++) {
				this.attributes.put(String.format("CLS_TO_%02d",i),this.to.get(i));
			}
			this.identifyCFnotification(this,prefix);
			if (!this.identifyCFreceiverUnreachable(this,prefix)) {
				if (!this.identifyCFreceiverAbsent(this,prefix)) {
					if (!this.identifyCFreceiverMailboxFull(this,prefix)) {
					}
				}
			}
		}

	// end of class AMessage
	}
		
		
	/* *************************************************************************
	 *		METHOD:			getSysProp
	 *		INPUT:			propname		- name of system property to be got
	 *		RESULT:			<value> of system property <propname>
	 *		DESCRIPTION:	gets <value> of system property <propname>
	 *						these values can be set by option "-D<propname>=<value>
	 *						default value (if property not set) is the empty string ""
	 *						the values can contain a set of "magic" string(s), 
	 *						which will be pre-processed:
	 *						magic string:	|	replaced with:
	 *						----------------+-----------------------------------
	 *						$20				|	a blank space " "
	 *						$SP$			|	a blank space " "
	 *						$PC$			|	a percent sign "%"
	 *										|
	 *
	 */
	public static String getSysProp(String propname) {
		String rawvalue = System.getProperty(propname);
		if (rawvalue==null) return "";
		return rawvalue.replace("$20"," ").replace("$PC$","%").replace("$SP$"," ");
	}


 public static void receiveEmailPOP(String pop3Host, String storeType, String user, String password) {
  try {
   Properties properties = new Properties();
   // properties.put("mail.sv-services.at", pop3Host);
   properties.put("securemail.a1.net", pop3Host);
   Session emailSession = Session.getDefaultInstance(properties);

   POP3Store emailStore = (POP3Store) emailSession.getStore(storeType);
   // Store emailStore = emailSession.getStore(storeType);
   
   emailStore.connect(user, password);

   Folder emailFolder = emailStore.getFolder("INBOX");
   emailFolder.open(Folder.READ_ONLY);

   Message[] messages = emailFolder.getMessages();
   for (int i = 0; i < messages.length; i++) {
	Message message = messages[i];
	System.out.println("---------------------------------");
	System.out.println("Email Number " + (i + 1));
	System.out.println("Subject: " + message.getSubject());
	System.out.println("From: " + message.getFrom()[0]);
	System.out.println("Text: " + message.getContent().toString());
   }

   emailFolder.close(false);
   emailStore.close();

  } catch (NoSuchProviderException e) {e.printStackTrace();} 
  catch (MessagingException e) {e.printStackTrace();}
  catch (IOException e) {e.printStackTrace();}
 }

	
	public static void listcontentmulti(Multipart cmulti, String prefix) throws MessagingException,IOException {
		int parts = cmulti.getCount();
		for (int j=0; j < parts; ++j) {
			// System.out.println(String.format("%s.%06d.Multipart Content:",prefix,j));
			MimeBodyPart part = (MimeBodyPart)cmulti.getBodyPart(j);
			listcontent(part.getContent(),String.format("%s.MP%06d",prefix,j));
		}
	}
	
	public static void listcontentstring(String cstr, String prefix) {
		if (cstr.length()>8000) cstr = cstr.substring(0,8000)+"...";
		cstr = cstr.replace("\r","");
		String clines[] = cstr.split("\n");
		for (int linum = 0; linum<clines.length; linum++) {
			System.out.println(String.format("%s.CL%06d=%s",prefix,linum,clines[linum]));
		}
	}
	
	public static void listcontentstream(InputStream istr, String prefix) {
		int c;
		// System.out.print(prefix + ".Stream=");
		StringBuilder sb = new StringBuilder();
		try {
		while ((c = istr.read()) != -1)
			sb.append(Character.toString((char)c));
		} catch (IOException e) {
			logger.error("listcontentstream: Exception: "+e);
		}
		listcontentstring(sb.toString(),prefix);
		// System.out.println();
	}
	
	public static void listcontent(Object content, String prefix) throws MessagingException,IOException {
		String msgtext;
		Class c;
		c = content.getClass();
		String classname = c.getCanonicalName();
		System.out.println(prefix + ".content_class="+classname);
		switch (classname) {
			case "javax.mail.internet.MimeMultipart":	listcontentmulti((Multipart)content,prefix);
														break;
			case "java.lang.String":					listcontentstring((String)content,prefix);
														break;
			case "com.sun.mail.imap.IMAPInputStream":	listcontentstream((InputStream)content,prefix);
														break;
			case "com.sun.mail.imap.IMAPNestedMessage":	listmessage((Message)content,prefix);
														break;
			default:
				msgtext = content.toString();
				if (msgtext.length()>80) msgtext = msgtext.substring(0,80)+"...";
				System.out.println(String.format("%s.Text=\"%s\"",prefix,msgtext));
		}
	}
	
	public static void listAddresses(Address[] addresses, String prefix) {
		if (addresses==null) {
			System.out.println(String.format("%s=<null>",prefix));
			return;
		} else if (addresses.length==0) {
			System.out.println(String.format("%s=<none>",prefix));
		} else if (addresses.length==1) {
			System.out.println(String.format("%s=%s",prefix,addresses[0]));
		} else {
			for (int ai = 0; ai < addresses.length; ai++) {
				System.out.println(String.format("%s[%06d]=%s",prefix,ai,addresses[ai]));
			}
		}
	}
	
	public static void listmessage(Message message, String prefix) throws MessagingException,IOException {
		System.out.println(prefix + ".---------------------------------");
		System.out.println(prefix + ".Subject=" + message.getSubject());
		listAddresses(message.getFrom(),prefix+".From");
		listAddresses(message.getAllRecipients(),prefix+".To");
		listcontent(message.getContent(),prefix);
	}

	public static void listmessages(Folder folder) throws MessagingException,IOException {
		Message[] messages = folder.getMessages();
		String foldername = folder.getName();
		String startmessageoption = System.getProperty("startmessage");
		String endmessageoption = System.getProperty("endmessage");
		int startmessage = (startmessageoption!=null)?Integer.parseInt(startmessageoption):0;
		int endmessage = (endmessageoption!=null)?Integer.parseInt(endmessageoption):startmessage+5;
		logger.debug(String.format("folder %s has %d messages",foldername,messages.length));
		for (int i = startmessage; ((i < messages.length) && (i<endmessage)); i++) {
			listmessage(messages[i],String.format((System.getProperty("logprefix") + ".MSG%06d"),i));
		}
	}
	
	/* aufgezeichnete Attribute:
		MSG_ID -						- (hoffentlich) eindeutige ID der klassifizierten Nachricht (per 20220218 DATNUM der Empfangszeit)
		MSG_RECEIVED 					- DATNUM, an der die klassifizierte Nachricht empfangen wurde
		CF_NOTIF_TYP 						- Art der CF-Benachrichtigung: CF_EINGELANGT / CF_URGENZ / UNBEKANNT
		CF_ZU_SVT_<2digitlaufnummer>		-	n. MST-ID des Trägers, ür den CF-Benachrichtigung versandt wurde
		CF_ZU_BKNR_<2digitlaufnummer>		-	n. Beitragskontonummer, für die CF-Benachrichtigung versandt wurde
		CF_ZU_SVTNAME_<2digitlaufnummer>	-	n. festgestellter Trägername, für den CF-Benachrichtigung versandt wurde
		CF_EMPF							- Empfänger-Email-Adresse der Glearingfall-Benachrichtigung (wenn die analysierte Nachricht eine war)
		CF_GESENDET						- sendezeit der CF-notification aus DGDIALOG
		CF_EMPFANGEN					- empfangszeit der CF-notification beim Empfänger
		BOUNCE_CLASSIFICATION			- Klassifizierung der empfangenen Nachricht: UNCLASSIFIED, UNREACHABLE, ABSENT
		UNREACH_EMAIL					- Email-Adresse, die nicht erreichbar ist
		CLS_FROM_<2digitlaufnummer>		- adresse, von der die analysierte Nachricht gesendet wurde
		CLS_TO_<2digitlaufnummer>		- addresse, an die die analysierte Nachricht gesendet wurde
		MSG_JSON						-	ganze Message (rekursiv mit Parts) im JSON-Format
	*/
	
	public void recordClassifyResult(AMessage amsg, CSVPrinter prt) {
		try {
			prt.printRecord(amsg.attributes.get("MSG_ID"),
							amsg.attributes.get("MSG_RECEIVED"),
							amsg.attributes.get("BOUNCE_CLASSIFICATION"),
							amsg.attributes.get("UNREACH_EMAIL"),
							amsg.attributes.get("CLS_FROM_00"),
							amsg.attributes.get("CLS_FROM_01"),
							amsg.attributes.get("CLS_FROM_02"),
							amsg.attributes.get("CLS_FROM_03"),
							amsg.attributes.get("CLS_FROM_04"),
							amsg.attributes.get("CLS_TO_00"),
							amsg.attributes.get("CLS_TO_01"),
							amsg.attributes.get("CLS_TO_02"),
							amsg.attributes.get("CLS_TO_03"),
							amsg.attributes.get("CLS_TO_04"),
							amsg.attributes.get("CF_NOTIF_TYP"),
							amsg.attributes.get("CF_ZU_SVT_00"),
							amsg.attributes.get("CF_ZU_SVT_01"),
							amsg.attributes.get("CF_ZU_SVT_02"),
							amsg.attributes.get("CF_ZU_SVT_03"),
							amsg.attributes.get("CF_ZU_SVT_04"),
							amsg.attributes.get("CF_ZU_BKNR_00"),
							amsg.attributes.get("CF_ZU_BKNR_01"),
							amsg.attributes.get("CF_ZU_BKNR_02"),
							amsg.attributes.get("CF_ZU_BKNR_03"),
							amsg.attributes.get("CF_ZU_BKNR_04"),
							amsg.attributes.get("CF_ZU_SVTNAME_00"),
							amsg.attributes.get("CF_ZU_SVTNAME_01"),
							amsg.attributes.get("CF_ZU_SVTNAME_02"),
							amsg.attributes.get("CF_ZU_SVTNAME_03"),
							amsg.attributes.get("CF_ZU_SVTNAME_04"),
							amsg.attributes.get("CF_EMPF"),
							amsg.attributes.get("CF_GESENDET"),
							amsg.attributes.get("CF_EMPFANGEN"),
							amsg.toJSONString()							// MSG_JSON
						);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void analyzeMessageBounce(Message message, String prefix, CSVPrinter prt) throws MessagingException,IOException {
		AMessage amsg = new AMessage();
		amsg.populateMessage(message);
		amsg.attributes.put("BOUNCE_CLASSIFICATION","UNCLASSIFIED");
		amsg.attributes.put("MSG_RECEIVED",dateToDatnum(amsg.rxdate));
		amsg.attributes.put("MSG_ID",uniqueIdFromDate(amsg.rxdate));
		logger.debug(prefix + ".------------BEGIN----------------");
		amsg.classify(prefix);
		recordClassifyResult(amsg,prt);
		amsg.listAttributes(prefix);
		logger.debug(prefix + ".-------------END-----------------");
	}
	
	public void analyzeMessagesBounces(Folder folder, Folder movetofolder) throws MessagingException,IOException {
		Message[] messages = folder.getMessages();
		String foldername = folder.getName();
		String startmessageoption = System.getProperty("startmessage");
		String endmessageoption = System.getProperty("endmessage");
		String afilenameoption = System.getProperty("afilename");
		String logprefix = System.getProperty("logprefix");
		int startmessage = (startmessageoption!=null)?Integer.parseInt(startmessageoption):0;
		int endmessage = (endmessageoption!=null)?Integer.parseInt(endmessageoption):startmessage+5;
		int msgcnt = messages.length;
		logger.debug(String.format(logprefix+".folder %s has %d messages",foldername,msgcnt));
		String afilename;
		if (afilenameoption==null) {
			afilename = "MSGBOUNCEANALYSIS_"+dateToDatnum(new Date())+".csv";
		} else {
			afilename = new String(afilenameoption);
		}
		CSVPrinter printer = new CSVPrinter(new FileWriter(afilename), CSVFormat.EXCEL);
		printer.printRecord("MSG_ID","MSG_RECEIVED","BOUNCE_CLASSIFICATION","UNREACH_EMAIL",
								"CLS_FROM_00","CLS_FROM_01","CLS_FROM_02","CLS_FROM_03","CLS_FROM_04",
								"CLS_TO_00","CLS_TO_01","CLS_TO_02","CLS_TO_03","CLS_TO_04",
								"CF_NOTIF_TYP",
								"CF_ZU_SVT_00","CF_ZU_SVT_01","CF_ZU_SVT_02","CF_ZU_SVT_03","CF_ZU_SVT_04",
								"CF_ZU_BKNR_00","CF_ZU_BKNR_01","CF_ZU_BKNR_02","CF_ZU_BKNR_03","CF_ZU_BKNR_04",
								"CF_ZU_SVTNAME_00","CF_ZU_SVTNAME_01","CF_ZU_SVTNAME_02","CF_ZU_SVTNAME_03","CF_ZU_SVTNAME_04",
								"CF_EMPF","CF_GESENDET","CF_EMPFANGEN","MSG_JSON");
		int analyzedmessages = 0;
		int copiedmessages = 0;
		int maxmessage = msgcnt-1;
		if (endmessage>maxmessage) endmessage = maxmessage;
		if (startmessage<0) startmessage = 0;
		int msgstoanalyze = endmessage-startmessage+1;
		logger.debug(String.format(getSysProp("logprefix")+".analyzeMessagesBounces.MSGS_TO_ANALYZE=%d",msgstoanalyze));
		for (int i = startmessage; i<=endmessage; i++) {
			logger.debug(String.format("analyzeMessagesBounces.MSG=%07d.OF=%07d.MSGSTOANALYZE=%07d",i,msgcnt,msgstoanalyze));
			analyzeMessageBounce(messages[i],String.format(logprefix+".MSG%06d",i),printer);
			if (movetofolder!=null) {
				Message mm[] = new Message[1];
				mm[0] = messages[i];
				folder.copyMessages(mm,movetofolder);					// copy message over to other folder
				copiedmessages++;
				messages[i].setFlag(Flags.Flag.DELETED, true);			// mark message as deleted to be able to EXPUNGE them afterwards
			}
			analyzedmessages++;
		}
		printer.close(true);		
		logger.debug(String.format(logprefix+".analyzeMessagesBounces.MSGSTOANALYZE=%06d.ANALYZED_MESSAGES=%06d.COPIED_MESSAGES=%06d.RESULT_FILE=%s",
									msgstoanalyze,analyzedmessages,copiedmessages,afilename));
		if ((movetofolder!=null) && (analyzedmessages==copiedmessages)) {
			if (analyzedmessages==copiedmessages) {
				Message[] expungedmessages = folder.expunge();	//  docs: https://docs.oracle.com/javaee/6/api/javax/mail/Folder.html#expunge()
				logger.debug(String.format(logprefix+".SUCCESS: permanently deleted %d messages after processing and copying them to folder \"%s\"",
											expungedmessages.length,movetofolder.getFullName()));
			} else {
				logger.debug(
					String.format(logprefix+".ERROR: did not expunge analyzed messages because analyzed message count (%06d) is not equal copied message count (%06d)",
									analyzedmessages,copiedmessages));
			}
		}
	}

	public void moveAllMessages(Folder folder, Folder movetofolder) throws MessagingException,IOException {
		Message[] messages = folder.getMessages();
		String foldername = folder.getName();
		String startmessageoption = System.getProperty("startmessage");
		String endmessageoption = System.getProperty("endmessage");
		String afilenameoption = System.getProperty("afilename");
		String logprefix = System.getProperty("logprefix");
		int startmessage = (startmessageoption!=null)?Integer.parseInt(startmessageoption):0;
		int endmessage = (endmessageoption!=null)?Integer.parseInt(endmessageoption):startmessage+5;
		int msgcnt = messages.length;
		logger.debug(String.format(logprefix+".moveAllMessages: folder %s has %d messages",foldername,msgcnt));
		String afilename;
		if (afilenameoption==null) {
			afilename = "MOVEALLMESSAGESLOG_"+dateToDatnum(new Date())+".csv";
		} else {
			afilename = new String(afilenameoption);
		}
		CSVPrinter printer = new CSVPrinter(new FileWriter(afilename), CSVFormat.EXCEL);
		printer.printRecord("MSG_NUM","INFO");
		int copiedmessages = 0;
		int processedmessages = 0;
		int maxmessage = msgcnt-1;
		if (endmessage>maxmessage) endmessage = maxmessage;
		if (startmessage<0) startmessage = 0;
		int msgstomove = endmessage-startmessage+1;
		logger.debug(String.format((logprefix+".moveAllMessages.MSGS_TO_MOVE=%d"),msgstomove));
		for (int i = startmessage; i<=endmessage; i++) {
			logger.debug(String.format((logprefix+".moveAllMessages.MSG=%07d.OF=%07d.MSGSTOMOVE=%07d"),i,msgcnt,msgstomove));
			messages[i].setFlag(Flags.Flag.SEEN,false);					// mark message as unseen (https://docs.oracle.com/javaee/7/api/javax/mail/Message.html)
			if (movetofolder!=null) {
				Message mm[] = new Message[1];
				mm[0] = messages[i];
				folder.copyMessages(mm,movetofolder);					// copy message over to other folder
				copiedmessages++;
				messages[i].setFlag(Flags.Flag.DELETED, true);			// mark message as deleted to be able to EXPUNGE them afterwards
				printer.printRecord(i,"COPIED,marked_for_deletion");
			} else {
				printer.printRecord(i,"not_moved");
			}
			processedmessages++;
		}
		printer.close(true);		
		logger.debug(String.format(logprefix+".moveAllMessages.MSGSTOMOVE=%06d.PROCESSEDMESSAGES=%06d.COPIEDMESSAGES=%06d.RESULT_FILE=%s",
									msgstomove,processedmessages,copiedmessages,afilename));
		if ((movetofolder!=null) && (processedmessages==copiedmessages)) {
			if (processedmessages==copiedmessages) {
				Message[] expungedmessages = folder.expunge();
				logger.debug(String.format(logprefix+".moveAllMessages.SUCCESS: permanently deleted %d messages after copying them to folder \"%s\"",
											expungedmessages.length,movetofolder.getFullName()));
			} else {
				logger.debug(
					String.format(logprefix+".ERROR: did not expunge analyzed messages because analyzed message count (%06d) is not equal copied message count (%06d)",
									processedmessages,copiedmessages));
			}
		}
	}


	public void saveMVBRFAUSWcontent(Object content, String svt, CSVPrinter prt) throws IOException, MessagingException {
		OutputStream out = null;
		InputStream in = null;
		String basepath = getSysProp("basepath");
		Pattern pattern = Pattern.compile("Auswertung_MeldungenClearingfaelle_([0-9]{14,14}).csv.gz");
		Matcher m;
		String timestamp;
		try {
			if (content instanceof Multipart) {
				Multipart multi = ((Multipart)content);
				int parts = multi.getCount();
				for (int j=0; j < parts; ++j) {
					MimeBodyPart part = (MimeBodyPart)multi.getBodyPart(j);
					if (part.getContent() instanceof Multipart) {
						// part-within-a-part, do some recursion...
						saveMVBRFAUSWcontent(part.getContent(),svt,prt);
					} else {
						String filename = part.getFileName();
						if (filename==null) {
							logger.debug(String.format("part %d has no filename, will be ignored",j));
							continue;
						}
						logger.debug(String.format("part %d has filename \"%s\"",j,filename));
						m = pattern.matcher(filename);
						if (m.matches()) {
							timestamp = m.group(1);
							logger.debug(String.format("timestamp for file in part %d is %s", j, timestamp));
						} else {
							continue;
						}
						if (!part.isMimeType("application/octet-stream")) {
							logger.debug(String.format("MIME-type of part %d is NOT application/octet-stream",j));
							String contentType = part.getContentType();
							if (contentType==null) contentType = new String("<null>");
							logger.debug(String.format("ContentType of part %d is \"%s\", will be stored nevertheless ...",j,contentType));
							logger.debug("part will be stored nevertheless, ...");
							prt.printComment(String.format(	"# MimeType of part %d ist NOT application/octet-stream, ContentType is \"%s\", will be stored nevertheless ...",
															j,contentType));
							Enumeration<String> hlist = part.getAllHeaderLines();
							String headerline;
							if (hlist.hasMoreElements()) logger.debug(String.format("Headers of part:",j));
							int hnum = 1;
							while (hlist.hasMoreElements()) {
								headerline = hlist.nextElement();
								logger.debug(String.format("Part %d, header line %d: %s",j,hnum,headerline));
								hnum++;
							}
							// continue;
						}
						File basedir = new File(basepath);
						if (!basedir.isDirectory()) {
							logger.debug(String.format("basepath %s does not exist",basepath));
							basedir = new File(basepath+"/");
							if (basedir.mkdirs()) {
								logger.debug(String.format("basepath %s created",basepath+"/"));
							} else {
								logger.error(String.format("basepath %s not created",basepath+"/"));
								System.exit(C_ERROR_INVALID_BASEPATH);
							}
						}
						int year    = Integer.parseInt(timestamp.substring(0,4));
						int month   = Integer.parseInt(timestamp.substring(4,6));
						int day     = Integer.parseInt(timestamp.substring(6,8));
						int hours   = Integer.parseInt(timestamp.substring(8,10));
						int minutes = Integer.parseInt(timestamp.substring(10,12));
						int seconds = Integer.parseInt(timestamp.substring(12,14));
												
						Calendar calsd = Calendar.getInstance();
						calsd.set(year-1900, month-1, day, hours, minutes, seconds);
						
						// logger.debug(String.format("file timestamp is %04d-%02d-%02d %02d:%02d:%02d - %s",year,month,day,hours,minutes,seconds, calsd.toString()));

						logger.debug(String.format("file timestamp is %04d-%02d-%02d %02d:%02d:%02d - %s",year,month,day,hours,minutes,seconds, calsd.toString()));
						
						if (hours>14 && hours<18) {
							logger.error(String.format("file timestamp %s is in dead time, cannot assign to sensible date", /* tsd.toString() */ calsd.toString()));
							continue;
						}

						if (hours>=18) {									// files from last evening belong to next day => add 1 day to timestamp date
							calsd.add(Calendar.DATE, 1); 
						}
						
						int repyear = calsd.get(Calendar.YEAR)+1900;
						int repmonth = calsd.get(Calendar.MONTH)+1;
						int repday = calsd.get(Calendar.DATE);

						// logger.debug(String.format("report timestamp is %04d-%02d-%02d - %s",repyear,repmonth,repday,tsd.toString()));
						
						logger.debug(String.format("report timestamp is %04d-%02d-%02d - %s",repyear,repmonth,repday,calsd.toString()));

						String ydname = String.format("%04d",repyear);
						String mdname = String.format("%04d%02d",repyear,repmonth);
						String ddname = String.format("%04d%02d%02d",repyear,repmonth,repday);							
						
						String path = basepath + "/" + ydname + "/" + mdname + "/" + ddname;
						
						boolean pathcreated = new File(path).mkdirs();
						logger.debug(String.format("part %d directory %s %s",j,path,(pathcreated?" had to be created":"was already there")));
						
						String filepath = path + "/" + svt + "_" + filename;
							
						logger.debug(String.format("part %d will be saved to %s",j,filepath));
						out = new FileOutputStream(new File(filepath));
						in = part.getInputStream();
						int savedbytes = 0;
						int k;
						byte copybuffer[] = new byte[CBUFSIZE];
						while ((k = in.read(copybuffer,0,CBUFSIZE)) != -1) {
							out.write(copybuffer,0,k);
							savedbytes += k;
							System.out.print(String.format("\r%012d",savedbytes));
						}
						System.out.println();
						logger.debug(String.format("part %d saved %d bytes to %s",j,savedbytes,filepath));
						prt.printRecord(svt,timestamp,filepath);
					}
				}
			}
		} finally {
			if (in != null) { in.close(); }
			if (out != null) { out.flush(); out.close(); }
		}
	}
   
   public void storeMVBRFAUSWmessages(Folder folder, Folder movetofolder, String maxagedays, String resultfilename) throws MessagingException,IOException {
	   
	   /*
	   SearchTerm subjectterm	= new SubjectTerm("Auswertung_MeldungenClearingfaelle");	// look for "Auswertung_MeldungenClearingfaelle"
	   SearchTerm ageterm		= new YoungerTerm(4*86400);									// look for messages younger than 4 days
	   SearchTerm searchterm	= new AndTerm(subjectterm,ageterm);							// messages must meet both criteria
	   */
	   SearchTerm searchterm	= new SubjectTerm("Auswertung_MeldungenClearingfaelle");	// look for "Auswertung_MeldungenClearingfaelle"
	   
	   Pattern pattern = Pattern.compile("([0-9]{2,2})Pr\\sAuswertung_MeldungenClearingfaelle");
	   
	   int maxdaysnum = Integer.parseInt(maxagedays);
	   
	   Date nowdate = new Date();
	   Calendar cal = Calendar.getInstance();
		cal.setTime(nowdate);
		cal.add(Calendar.DATE, 0-maxdaysnum);
		Date oldestdate = cal.getTime();
		logger.debug(String.format("storeMVBRFAUSWmessages: oldest messages may be from %s",dateToString(oldestdate)));
	   
	   Message[] messages = folder.search(searchterm);
	   String foldername = folder.getName();
		logger.debug(String.format("storeMVBRFAUSWmessages: folder %s has %d matching messages",foldername,messages.length));
		
		String subject, msgtext;
		Matcher m;
		String svt;
		Message message;
		Date rxdate;
		
		CSVPrinter printer = new CSVPrinter(new FileWriter(resultfilename), CSVFormat.EXCEL);
		printer.printRecord("SVT","FTIMESTAMP","FULLFILENAME");
	
		// process all candidate mesasges
		for (int i = 0; i<messages.length; i++) {
			message = messages[i];
			subject = message.getSubject();
			rxdate = message.getReceivedDate();
			if (subject==null) continue;
			if (rxdate.compareTo(oldestdate)<0) continue;			// if message is older than 4 days, ignore
			// logger.debug(String.format("%08d - Received: %s, Subject: %s",i+1,message.getReceivedDate(),subject)); 
			m = pattern.matcher(subject);
			if (m.matches()) {
				svt = m.group(1);
				logger.debug(String.format("found MVBRFAUSW for SVT=%s, Received %s, subject: %s", svt, message.getReceivedDate(), subject));
				saveMVBRFAUSWcontent(message.getContent(),svt,printer);
				if (movetofolder!=null) {
					Message mm[] = new Message[1];
					mm[0] = message;
					folder.copyMessages(mm,movetofolder);			// copy message over to other folder
					message.setFlag(Flags.Flag.DELETED, true);		// mark original message as deleted
				}
			}
		}
		if (movetofolder!=null) {
			Message[] expungedmessages = folder.expunge();
			logger.debug(String.format("permanently deleted %d messages after processing and copying them to folder \"%s\"",expungedmessages.length,movetofolder.getFullName()));
		}
		printer.close(true);
		logger.debug(String.format("saved result data to file %s",resultfilename));
   }

public void receiveEmailIMAP(	String hostname, String storeType, 
								String user, String password, 
								String foldername, String operation, String maxagedays) {
  try {
   Properties properties = new Properties();
   Session emailSession = Session.getDefaultInstance(properties);
   
   logger.debug("got emailSession");

   Store emailStore = emailSession.getStore(storeType);
   
   logger.debug(String.format("got emailStore, will connect to host %s with username=\"%s\" and password=\"%s\"",
								hostname,
								user,
								(getSysProp("secret").equals("show")?password:"<hidden>")));
   
   emailStore.connect(hostname, user, password);   
   logger.debug(String.format("receiveEmailIMAP: connected to host %s with username %s",hostname,user));

   Folder df = emailStore.getDefaultFolder();
   String dfn = df.getName();
   logger.debug(String.format("default folder has name \"%s\"",dfn));
   
   boolean folderfound = false;
   String cfn;
   
   String movetofoldername = getSysProp("movetofoldername");
   Folder movetofolder = null;
   
   String resultfilename = getSysProp("resultfilename");
   
   Folder sfl[] = df.list("*");
   logger.debug(String.format("receiveEmailIMAP: folder \"%s\" has %d sub-folders:",dfn,sfl.length));
   for (int i = 0; i < sfl.length; i++) {
	   cfn = sfl[i].getName();
	   if (cfn.equals(foldername)) folderfound = true;
	   if (movetofoldername!=null) {
		   if (cfn.equals(movetofoldername)) movetofolder = sfl[i];
	   }
	   if (operation.equals("listfolders")) {
			logger.debug(String.format("receiveEmailIMAP:  sub-folder #%05d has name \"%s\"",i+1,sfl[i].getFullName()));
	   }
   }
   
   if (!folderfound) {
	   logger.error(String.format("receiveEmailIMAP: Mail Folder %s not found as subfolder of %s",foldername,dfn));
   }

   Folder emailFolder = emailStore.getFolder(foldername);
   logger.debug(String.format("receiveEmailIMAP: could find email folder %s",foldername));
   
   emailFolder.open(Folder.READ_WRITE);
   logger.debug(String.format("receiveEmailIMAP: opened Folder %s READ_WRITE",foldername));
   
   switch (operation) {
		case "RX432":			storeMVBRFAUSWmessages(emailFolder,movetofolder,maxagedays,resultfilename);
								break;
		case "listfolders":		break;
		case "listmessages":	listmessages(emailFolder);
								break;
		case "RX57":			this.analyzeMessagesBounces(emailFolder,movetofolder);
								break;
		case "moveallmessages": this.moveAllMessages(emailFolder,movetofolder);
								break;
		default:				listmessages(emailFolder);
   }

   emailFolder.close(false);
   emailStore.close();

  } catch (NoSuchProviderException e) {e.printStackTrace();} 
  catch (MessagingException e) {e.printStackTrace();}
  catch (IOException e) {e.printStackTrace();}
 }

/* *****************************************************************************
 *		METHOD:			main
 *		DESCRIPTION:	ReceiveMail main program, invokes protocol-specific 
 *						receive method(s)
 */
 public static void main(String[] args) {
	 
	ReceiveMail rm = new ReceiveMail();

  String host = "mail.sv-services.at"; //change accordingly
  // String host = "securemail.a1.net"; //change accordingly
  // String mailStoreType = "pop3";
  String mailStoreType = "imap";
  

  // String username= "99noreply-clearing";
  // String username = "noreply-clearing@sozialversicherung.at";
  // String password = "66o%!3J+Ad28";
  // String folderName = "INBOX";

  
  // this worked: 
  /*
  String username = "wolfgang.scherer@itsv.at";
  String password = "18Jun2K211!";					// wolfgang's OLD password !!
  String folderName = "INBOX";
  */
  
  String username	= getSysProp("username");
  String password	= getSysProp("password");
  String foldername	= getSysProp("foldername");
  String operation	= getSysProp("operation");
  String maxagedays = getSysProp("maxagedays");
  
  if (mailStoreType.equals("pop3")) {
	  receiveEmailPOP(host, mailStoreType, username, password);
  } else if (mailStoreType.equals("imap")) {
	  rm.receiveEmailIMAP(host, mailStoreType, username, password, foldername, operation, maxagedays);
  }
  
 }
}