Pascal Peridont 19 лет назад
Родитель
Сommit
0a728c6210

+ 244 - 0
std/mtwin/mail/Browser.hx

@@ -0,0 +1,244 @@
+package mtwin.mail;
+
+signature MainPart {
+	ctype_primary: String,
+	ctype_secondary: String,
+	charset: String,
+	content: String
+}
+
+import neko.Utf8;
+import mtwin.mail.Part;
+
+class Browser extends MetaPart<Browser> {
+
+	public static function parseString( str : String ) : Browser {
+		var o = new Browser();
+		o.parse( str );
+		return o;
+	}
+
+	//////////
+
+	public function new(?ctype : String, ?sp : Bool, ?charset : String){
+		if( ctype == null ) ctype = "text/plain";
+		if( sp == null ) sp = false;
+		if( charset == null ) charset = "iso-8859-15";
+		super( ctype, sp, charset );
+	}
+
+	public override function newPart( ctype : String ) : Browser {
+		var o = new Browser( ctype, true, charset );
+		this.addPart( o );
+		return o;
+	} 
+
+	public function getMainPartCharset( cs : String ){
+		var r = getMainPart();
+		if( cs != r.charset ){
+			var cslc = cs.toLowerCase();
+			var charsetlc = r.charset.toLowerCase();
+
+			if( cslc != "utf-8" && charsetlc == "utf-8" ){
+				r.content =  Utf8.decode( r.content );
+			}else if( charsetlc != "utf-8" && cslc == "utf-8" ){
+				r.content =  Utf8.encode( r.content );
+			}
+			r.charset = cs;
+		}
+		return r;
+	}
+
+	public function getMainPart( ?level : Int, ?priority : Int, ?cpriority : Int ) : MainPart {
+		if( level == null ) level = 0;
+		if( priority == null ) priority = 0;
+		if( cpriority == null ) cpriority = 0;
+
+		var ctype = contentType.split("/");
+		var ctype0 = ctype[0];
+		var ctype1 = ctype[1];
+
+		if( ctype0 != "multipart" || (level == 0 && parts.length == 0) ){
+			if( level == 0 ) return mkBody();
+			if( ctype1 == "html" ) return mkBody();
+			if( ctype1 == "plain" && cpriority > 0 ) return mkBody();
+		}else{
+			if( level == 0 ){
+				// multipart !
+				// si c'est au premier niveau, c'est une boucle principale, avec priorité qui augmente
+				do {
+					do {
+						var r = null;
+						for( part in parts ){
+							r = part.getMainPart( level + 1, priority, cpriority );
+							if( r != null ) break;
+						}
+						if( r != null ) return r;
+						priority++;
+					}while( priority <= 1 );
+					cpriority++;
+				}while( cpriority <= 1 );
+			}else{
+				// là c'est des boucles qui se déclanche si c'est ok
+				if( ctype1 == "alternative" || priority > 0 ){
+					var r = null;
+					for( part in parts ){
+						r = part.getMainPart( level + 1, priority, cpriority );
+						if( r != null ) return r;
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	public function listAttachment( ?level : Int ){
+		if( level == null ) level = 0;
+		var l = listAttachmentObjects( level );
+		var r = new List();
+		for( v in l ){
+			r.add({
+				name: v.name, 
+				id: v.id, 
+				type: v.contentType
+			});
+		}
+		return r;
+	}
+
+	function listAttachmentObjects( level : Int ) : List<Browser> {
+		var ctype = contentType.split("/");
+		var ctype0 = ctype[0];
+		var ctype1 = ctype[1];
+
+		var ret = new List();
+		if( ctype0 != "multipart" ){
+			if( level != 0 && headers.exists("Content-Disposition") ){
+				ret.add( this );
+			}
+		}else if( ctype1 != "alternative" ){
+			for( part in parts ){
+				for( v in part.listAttachmentObjects( level + 1 ) ){
+					ret.add( v );
+				}
+			}
+		}
+		return ret;
+	}
+
+	public function getAttachment( i : String ) : {name: String,ctype: String,content: String }{
+		if( id == i ){
+			return {
+				name: name,
+				ctype: contentType,
+				content: content
+			};
+		}
+		for( part in parts ){
+			var t = part.getAttachment( i );
+			if( t != null ) return t;
+		}
+		return null;
+	}
+
+	public function getAttachmentByCid( cid : String ) : {name: String,ctype: String,content: String }{
+		var cid = getHeader("Content-Id");
+		if( cid != null && StringTools.trim(cid) == "<"+cid+">" ){
+			return {
+				name: name,
+				ctype: contentType,
+				content: content
+			};
+		}
+		for( part in parts ){
+			var t = part.getAttachmentByCid( cid );
+			if( t != null ) return t;
+		}
+		return null;
+	}
+	
+	public function hasHeader( name ){
+		name = Tools.formatHeaderTitle( name );
+		return headers.exists( name );
+	}
+
+	function mkBody() : {ctype_primary: String,ctype_secondary: String,charset: String,content: String} {
+		var ctype = contentType.split("/");
+
+		return {
+			ctype_primary: ctype[0],
+			ctype_secondary: ctype[1],
+			charset: charset,
+			content: content
+		};
+	}
+
+	public function toString( ?level : Int ) : String {
+		if( level == null ) level = 0;
+
+		var s = StringTools.lpad("","\t",level);
+		var s2 = StringTools.lpad("","\t",level+1);
+		var sb = new StringBuf();
+		
+		sb.add( s );
+		sb.add("mail.Browser#");
+		sb.add(id);
+		sb.add("<");
+		sb.add(contentType);
+		sb.add(">");
+
+		var sb2 = new StringBuf();
+
+		if( hasHeader("From") ){
+			sb2.add(s2);
+			sb2.add("From: ");
+			sb2.add(getHeader("From","utf-8"));
+			sb2.add("\n");
+		}
+
+		if( hasHeader("To") ){
+			sb2.add(s2);
+			sb2.add("To: ");
+			sb2.add(getHeader("To","utf-8"));
+			sb2.add("\n");
+		}
+
+		if( hasHeader("Subject") ){
+			sb2.add(s2);
+			sb2.add("Subject: ");
+			sb2.add(getHeader("Subject","utf-8"));
+			sb2.add("\n");
+		}
+
+		if( hasHeader("Date") ){
+			sb2.add(s2);
+			sb2.add("Date: ");
+			sb2.add(getHeader("Date","utf-8"));
+			sb2.add("\n");
+		}		
+
+		if( name != null ){
+			sb2.add(s2);
+			sb2.add("Name: ");
+			sb2.add(name);
+			sb2.add("\n");
+		}
+
+		for( part in parts ){
+			sb2.add( part.toString( level + 1 ) );
+		}
+
+		var t = sb2.toString();
+
+		if( t.length > 0 ){
+			sb.add(" [\n");
+			sb.add(t);
+			sb.add(s);
+			sb.add("]");
+		}
+		
+		sb.add("\n");
+		return sb.toString();
+	}
+
+}

+ 13 - 0
std/mtwin/mail/Exception.hx

@@ -0,0 +1,13 @@
+package mtwin.mail;
+
+class Exception {
+	var s : String;
+
+	public function new(s){
+		this.s = s;
+	}
+
+	public function toString(){
+		return s;
+	}
+}

+ 302 - 0
std/mtwin/mail/Imap.hx

@@ -0,0 +1,302 @@
+package mtwin.mail;
+
+class ImapException extends mtwin.mail.Exception {
+}
+
+signature ImapConnectionInfo {
+	host: String, 
+	port: Int, 
+	user: String, 
+	pass: String
+}
+
+signature ImapMailbox {
+	name: String,
+	flags: List<String>,
+	hasChildren: Bool
+}
+
+import neko.Socket;
+
+class Imap {
+	public static var DEBUG = false;
+
+	var cnx : Socket;
+	var count : Int;
+
+	static var REG_CRLF = ~/\r?\n/g;
+	function rmCRLF(s){
+		return REG_CRLF.replace(s, "");
+	}
+
+	function debug(s:String){
+		if( DEBUG ) neko.Lib.print(Std.string(s)+"\n");
+	}
+
+	function quote( s : String ) : String {
+		return "\""+s.split("\"").join("\\\"")+"\"";
+	}
+	
+	public function new(args: ImapConnectionInfo){
+		count = 0;
+		cnx = new Socket();
+		connect( args.host, args.port );
+		login( args.user, args.pass );
+	}
+
+	function command( command, args, r ){
+		count++;
+		var c = Std.string(count);
+		c = StringTools.lpad(c,"A000",4);
+		cnx.write( c+" "+command+" "+args+"\r\n" );
+		debug( "S: "+c+" "+command+" "+args );
+
+		if( !r ){
+			return null;
+		}
+		return read(c);
+	}
+
+	static var REG_RESP = ~/(OK|NO|BAD) (\[([^\]]+)\] )?(([A-Z]{2,}) )? ?(.*)/;
+	function read( c ){
+		var resp = new List();
+		var sb : StringBuf = null;
+		while( true ){
+			var line = cnx.readLine();
+			debug("R: "+line);
+			line = rmCRLF(line);
+
+			if( c != null && line.substr(0,4) == c ){
+				if( REG_RESP.match(line.substr(5,line.length-5)) ){
+					if( sb != null ){
+						resp.add( sb.toString() );
+					}
+					return {
+						result: resp,
+						success: REG_RESP.matched(1) == "OK",
+						error: REG_RESP.matched(1),
+						command: REG_RESP.matched(4),
+						response: REG_RESP.matched(6),
+						comment: REG_RESP.matched(3)
+					};
+				}else{
+					throw new ImapException("Unknow response : "+line);
+				}
+			}else{
+				if( StringTools.startsWith(line,"* ") ){
+					if( sb != null ){
+						resp.add( sb.toString() );
+					}
+					sb = new StringBuf();
+					sb.add( line.substr(2,line.length - 2) );
+				}else{
+					if( sb != null ){
+						sb.add( line+"\r\n" );
+					}else{
+						resp.add( line );
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	function connect( host : String, port : Int ){
+		try{
+			cnx.connect( Socket.resolve(host), port );
+		}catch( e : Dynamic ){
+			throw new ImapException("Unable to connect to imap server "+host+" on port "+port);
+		}
+		debug("socket connected");
+		cnx.setTimeout( 1 );
+		cnx.readLine();
+	}
+
+	function login( user : String, pass : String ){	
+		var r = command("LOGIN",user+" "+pass,true);
+		if( !r.success ){
+			throw new ImapException("Error on login: "+r.response);
+		}
+	}
+	
+	/////////
+	//
+	static var REG_EXISTS = ~/^([0-9]+) EXISTS$/;
+	static var REG_RECENT = ~/^([0-9]+) RECENT$/;
+	static var REG_UNSEEN = ~/^OK \[UNSEEN ([0-9]+)\]/;
+	
+	public function select( mailbox : String ){
+		var r = command("SELECT",quote(mailbox),true);
+		if( !r.success ) 
+			throw new ImapException("Unable to select "+mailbox+": "+r.response);
+		
+		var ret = {recent: 0,exists: 0,firstUnseen: null};
+		for( v in r.result ){
+			if( REG_EXISTS.match(v) ){
+				ret.exists = Std.parseInt(REG_EXISTS.matched(1));
+			}else if( REG_UNSEEN.match(v) ){
+				ret.firstUnseen = Std.parseInt(REG_UNSEEN.matched(1));
+			}else if( REG_RECENT.match(v) ){
+				ret.recent = Std.parseInt(REG_RECENT.matched(1));
+			}
+		}
+
+		return ret;
+	}
+
+	static var REG_LIST_RESP = ~/LIST \(([ \\A-Za-z0-9]*)\) "\." "([^"]+)"/;
+	public function mailboxes( ?pattern : String ) : List<ImapMailbox> {
+		var r;
+		if( pattern == null ){
+			r = command("LIST","\".\" \"*\"",true);
+		}else{
+			r = command("LIST","\".\" \""+pattern+"\"",true);
+		}
+		if( !r.success ){
+			throw new ImapException("Unable to list mailboxes (pattern: "+pattern+"): "+r.response);
+		}
+
+		var ret = new List();
+		for( v in r.result ){
+			if( REG_LIST_RESP.match(v) ){	
+				var name = REG_LIST_RESP.matched(2);
+				var flags = REG_LIST_RESP.matched(1).split(" ");
+
+				var t = {name: name,flags: new List(),hasChildren: false};
+
+				for( v in flags ){
+					if( v == "" ) continue;
+					
+					if( v == "\\HasNoChildren" ){
+						t.hasChildren = false;
+					}else if( v == "\\HasChildren" ){
+						t.hasChildren = true;
+					}
+
+					t.flags.add( v );
+				}
+
+				ret.add(t);
+			}
+		}
+		return ret;
+	}
+
+	public function search( ?pattern : String ){
+		if( pattern == null ) pattern = "ALL";
+		var r = command("SEARCH",pattern,true);
+		if( !r.success ){
+			throw new ImapException("Unable to search messages (pattern: "+pattern+"): "+r.response);
+		}
+
+		var l = new List();
+
+		for( v in r.result ){
+			if( StringTools.startsWith(v,"SEARCH ") ){
+				var t = v.substr(7,v.length-7).split(" ");
+				for( i in t ){
+					l.add( Std.parseInt(i) );
+				}
+			}
+		}
+
+		return l;
+	}
+
+	public function fetchSearch( pattern : String, ?section : String ){
+		if( section == null ) section = "BODY.PEEK[]";
+		var r = search(pattern);
+		if( r.length == 0 ) return new IntHash();
+
+		return fetchRange( r.join(","), section );
+	}
+
+	public function fetchOne( id : Int, ?section : String, ?useUid : Bool ) {
+		if( section == null ) section = "BODY.PEEK[]";
+		if( useUid == null ) useUid = false;
+
+		var r = fetchRange( Std.string(id), section, useUid );
+		if( !r.exists(id) ){
+			throw new ImapException("fetchOne failed");
+		}
+		return r.get(id);
+	}
+
+	static var REG_FETCH_MAIN = ~/([0-9]+) FETCH \(/;
+	static var REG_FETCH_PART = ~/^(BODY\[[A-Za-z0-9.]*\]|RFC822\.?[A-Z]*) \{([0-9]+)\}/;
+	static var REG_FETCH_FLAGS = ~/^FLAGS \(([ \\A-Za-z0-9$]*)\) */;
+	static var REG_FETCH_UID = ~/^UID ([0-9]+) */;
+	static var REG_FETCH_BODYSTRUCTURE = ~/^BODY(STRUCTURE)? \(/;
+	static var REG_FETCH_END = ~/^([A0-9]{4}) (OK|BAD|NO)/;
+	public function fetchRange( range : String, ?section : String, ?useUid : Bool ){
+		if( range == null ) return null;
+		if( section == null ) section = "BODY[]";
+		if( useUid == null ) useUid = false;
+		
+		if( useUid )
+			command("UID FETCH",range+" "+section,false);
+		else
+			command("FETCH",range+" "+section,false);
+
+		var ret = new IntHash();
+		while( true ){
+			var l = cnx.readLine();
+			if( REG_FETCH_MAIN.match(l) ){
+				var id = Std.parseInt(REG_FETCH_MAIN.matched(1));
+				
+				var o = if( ret.exists(id) ){
+					ret.get(id); 
+				}else {
+					var o = {type: null,content: null,flags: null,uid: null,structure: null};
+					ret.set(id,o);
+					o;
+				}
+
+				var s = REG_FETCH_MAIN.matchedRight();
+				while( s.length > 0 ){
+					if( REG_FETCH_FLAGS.match( s ) ){
+						o.flags = REG_FETCH_FLAGS.matched(1).split(" ");
+						s = REG_FETCH_FLAGS.matchedRight();
+					}else if( REG_FETCH_UID.match( s ) ){
+						o.uid = Std.parseInt(REG_FETCH_UID.matched(1));
+						s = REG_FETCH_UID.matchedRight();
+					}else if( REG_FETCH_BODYSTRUCTURE.match( s ) ){
+						var t = REG_FETCH_BODYSTRUCTURE.matchedRight().substr(0,-1);
+						o.structure = ImapBodyStructure.parse( t );
+						break;
+					}else if( REG_FETCH_PART.match( s ) ){
+						var type = REG_FETCH_PART.matched(1);
+						var len = Std.parseInt(REG_FETCH_PART.matched(2));
+						
+						o.content = cnx.read( len );
+						o.type = type;
+						cnx.readLine();
+						break;
+					}else{
+						break;
+					}
+				}
+				
+			}else if( REG_FETCH_END.match(l) ){
+				var resp = REG_FETCH_END.matched(2);
+				if( resp == "OK" ){
+					break;
+				}else{
+					throw new ImapException("Error fetching messages : "+l);
+				}
+			}else{
+				throw new ImapException("Unknow response from fetch : "+l);
+			}
+		}
+		
+		if( useUid ){
+			var old = ret;
+			ret = new IntHash();
+			for( e in old ){
+				ret.set(e.uid,e);
+			}
+		}
+
+		return ret;
+	}
+}

+ 203 - 0
std/mtwin/mail/ImapBodyStructure.hx

@@ -0,0 +1,203 @@
+package mtwin.mail;
+
+class ImapBodyStructure {
+	public var ctype0: String;
+	public var ctype1: String;
+	public var params : Hash<String>;
+	public var parts: List<ImapBodyStructure>;
+
+	// single-part specific
+	public var id: String;
+	public var description : String;
+	public var encoding : String;
+	public var size : Int;
+	public var disposition : String;
+	public var dispositionParams : Hash<String>;
+
+	//
+	public var __length : Int;
+	public var imapId : String;
+
+	public function new(){
+		parts = new List();
+		params = new Hash();
+	}
+
+	public function getMainPart( ?level : Int, ?priority : Int, ?cpriority : Int ) : ImapBodyStructure {
+		if( level == null ) level = 0;
+		if( priority == null ) priority = 0;
+		if( cpriority == null ) cpriority = 0;
+
+		if( ctype0 != "multipart" || (level == 0 && parts.length == 0) ){
+			if( level == 0 ) return this;
+			if( ctype1 == "html" ) return this;
+			if( ctype1 == "plain" && cpriority > 0 ) return this;
+		}else{
+			if( level == 0 ){
+				// multipart !
+				// si c'est au premier niveau, c'est une boucle principale, avec priorité qui augmente
+				do {
+					do {
+						var r = null;
+						for( part in parts ){
+							r = part.getMainPart( level + 1, priority, cpriority );
+							if( r != null ) break;
+						}
+						if( r != null ) return r;
+						priority++;
+					}while( priority <= 1 );
+					cpriority++;
+				}while( cpriority <= 1 );
+			}else{
+				// là c'est des boucles qui se déclanche si c'est ok
+				if( ctype1 == "alternative" || priority > 0 ){
+					var r = null;
+					for( part in parts ){
+						r = part.getMainPart( level + 1, priority, cpriority );
+						if( r != null ) return r;
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	public function listAttachment( ?level : Int ) : List<ImapBodyStructure> {
+		if( level == null ) level = 0;
+		var ret = new List();
+		if( ctype0 != "multipart" ){
+			if( level != 0 && disposition != null ){
+				ret.add( this );
+			}
+		}else if( ctype1 != "alternative" ){
+			for( part in parts ){
+				for( v in part.listAttachment( level + 1 ) ){
+					ret.add( v );
+				}
+			}
+		}
+		return ret;
+	}
+
+	public static function parse( s : String, ?id : String ) : ImapBodyStructure{
+		if( id == null ) id = "";
+		var len = s.length;
+		var parCount = 0;
+		var p = 0;
+		var ret = new ImapBodyStructure();
+		ret.imapId = id;
+		var addPart = function( p ){
+			ret.parts.add( p );
+		};
+		var tmp = {pName: null,argPos: 0};
+		var addElement = function( e : String ){
+			if( ret.ctype0 == null ){
+				ret.ctype0 = e;
+			}else if( ret.ctype1 == null ){
+				ret.ctype1 = e;
+				tmp.argPos = 0;
+			}else{
+				if( e == "NIL" ) return;
+				if( ret.ctype0 == "multipart" ){
+					switch( tmp.argPos ){
+						case 1:
+							if( tmp.pName == null ) 
+								tmp.pName = e;
+							else{
+								ret.params.set(tmp.pName,e);
+								tmp.pName = null;
+							}
+						case 4:
+						case 5:
+					}
+				}else{
+					switch( tmp.argPos ){
+						case 1:
+							if( tmp.pName == null ) 
+								tmp.pName = e;
+							else{
+								ret.params.set(tmp.pName,e);
+								tmp.pName = null;
+							}
+						case 2:
+							ret.id = e;
+						case 3:
+							ret.description = e;
+						case 4:
+							ret.encoding = e;
+						case 5:
+							ret.size = Std.parseInt(e);
+						default:
+							var dispoPos = if( ret.ctype0 == "text" ) 8 else if( ret.ctype0 == "message" ) 10 else 7;
+							if( tmp.argPos == dispoPos ){
+								if( parCount == 1 ){
+									ret.disposition = e;
+									ret.dispositionParams = new Hash();
+								}else{
+									if( tmp.pName == null ) 
+										tmp.pName = e;
+									else{
+										ret.dispositionParams.set(tmp.pName,e);
+										tmp.pName = null;
+									}
+								}
+							}
+					}
+				}
+				
+			}
+		};
+		while( p < len ){
+			var c = s.charAt(p);
+			p++;
+			switch( c ){
+				case "(":
+					if( ret.ctype1 == null ){
+						var newPart = parse( s.substr(p,s.length-p), (if( id == "" ) "" else id + "." )+ (ret.parts.length+1) );
+						addPart( newPart );
+						ret.ctype0 = "multipart";
+						p += newPart.__length;
+					}else
+						parCount++;
+				case ")":
+					parCount--;
+					if( parCount < 0 ){
+						ret.__length = p;
+						return ret;
+					}
+				case "\"":
+					var b = new StringBuf();
+					var prev = null;
+					while( p < len ){
+						var c2 = s.charAt(p);
+						p++;
+						if( c2 == "\"" && prev != "\\" )
+							break;
+						b.add( c2 );
+						prev = c2;
+					}
+					addElement( b.toString() );
+				case " ":
+					if( parCount == 0 ){
+						tmp.argPos++;
+					}
+				default:
+					var b = new StringBuf();
+					p--;
+					while( p < len ){
+						var c2 = s.charAt(p);
+						p++;
+						if( c2 == ")" || c2 == " " ){
+							p--;
+							break;
+						}
+						b.add( c2 );
+					}
+					addElement( b.toString() );
+			}
+		}
+		ret.__length = p;
+		return ret;
+	}
+
+}

+ 378 - 0
std/mtwin/mail/Part.hx

@@ -0,0 +1,378 @@
+package mtwin.mail;
+
+import neko.Utf8;
+
+class Part extends MetaPart<Part> {
+	public static function parseString( str : String ) : Part {
+		var o = new Part();
+		o.parse( str );
+		return o;
+	}
+
+	public function new(?a,?b,?c){
+		super(a,b,c);
+	}
+
+	public override function newPart( ctype : String ) : Part {
+		var o = new Part( ctype, true, charset );
+		this.addPart( o );
+		return o;
+	}
+}
+
+class MetaPart<T> {
+	static var headerOrder = [
+		"Return-Path","Received","Date","From","Subject","Sender","To",
+		"Cc","Bcc","Content-Type","X-Mailer","X-Originating-IP","X-Originating-User"
+	];
+
+	static var REG_HEADER = ~/^([a-zA-Z0-9_\-]+):(.*)$/;
+	static var REG_CRLF_END = ~/(\r?\n)$/;
+	
+	//////////////////
+	
+	public var content : String;
+	public var parts : List<T>;
+	var headers : Hash<Array<String>>;
+	var contentType : String;
+	var charset : String;
+	var boundary : String;
+	public var name : String;
+	var id : String;
+	var subPart : Bool;
+
+	public function new( ?ctype : String, ?sp : Bool, ?charset : String ){
+		this.contentType = if( ctype == null ) "text/plain" else ctype;
+		this.subPart = sp == true;
+		this.charset = if( charset == null ) "iso-8859-1" else charset;
+
+		content = "";
+		
+		parts = new List();
+		headers = new Hash();
+	}
+
+	function getContentType(){
+		return contentType;
+	}
+
+	public function addPart( part : T ){
+		parts.push( part );
+	}
+
+	public function setContent( c : String ){
+		setHeader( "Content-Transfer-Encoding", "quoted-printable" );
+		content = c;
+	}
+
+	public function setContentFromFile(filename:String,type:String){
+		var a = filename.split("/");
+		name = a.pop();
+		content = neko.File.getContent(filename);
+		contentType = type;
+		setHeader("Content-Type",type+"; name=\""+name+"\"");
+		setHeader("Content-Disposition","attachment; filename=\""+name+"\"");
+		setHeader("Content-Transfer-Encoding","base64");
+	}
+	
+	public function setHeader( name : String, content : String ){
+		if( headers.exists(name) ){
+			var l = headers.get(name);
+			if( l.length > 1 )
+				throw "Unable to setHeader, multiple header.";
+
+			l[0] = content;
+		}else{
+			headers.set(name,[content]);
+		}
+	}
+
+	public function getHeader( name: String, ?cs : String ){
+		if( !headers.exists(name) ){
+			return null;
+		}
+		var r = headers.get(name)[0];
+
+		if( cs != null && cs != charset ){
+			var cslc = cs.toLowerCase();
+			var charsetlc = charset.toLowerCase();
+			if( cslc != "utf-8" && charsetlc == "utf-8" ){
+				r =  Utf8.decode( r );
+			}else if( charsetlc != "utf-8" && cslc == "utf-8" ){
+				r =  Utf8.encode( r );
+			}
+		}
+
+		return r;
+	}
+
+	public function addHeader( name : String, content : String ){
+		if( headers.exists(name) ){
+			headers.get(name).push(content);
+		}else{
+			headers.set(name,[content]);
+		}		
+	}
+
+	public function setDate( ?d : Date ){
+		if( d == null ) d = Date.now();
+		setHeader("Date",DateTools.format(d,"%a, %e %b %Y %H:%M:%S %z"));
+	}
+
+	public function setContentId( ?cid : String ) : String {
+		if( cid == null ){
+			var t = getHeader("Content-Id");
+			if( t != null ){
+				return t.substr(1,t.length-2);
+			}
+
+			cid = Tools.randomEight()+"."+Tools.randomEight();
+
+			setHeader("Content-Id","<"+cid+">");
+		}else{
+			setHeader("Content-Id","<"+cid+">");
+		}
+
+		return cid;
+	}
+
+	public function htmlUseContentId( p : Hash<String> ){
+		for( filename in p.keys() ){
+			content = StringTools.replace( content, filename, "cid:"+p.get(filename) );
+		}
+	}
+
+	static var REG_START_TAB = ~/^(\t| )+/;
+	function htmlRemoveTab(){
+		content = REG_START_TAB.replace(content,"");
+	}
+
+	public function get() : String {
+		var boundary = "";
+
+		if( parts.length > 0 ){
+			if( contentType.substr(0,10).toLowerCase() != "multipart/" ){
+				contentType = "multipart/mixed";
+			}
+
+			if( boundary == null || boundary.length == 0 ){
+				boundary = "----=" + Tools.randomEight() + "_" + Tools.randomEight() + "." + Tools.randomEight();
+			}
+
+			setHeader("Content-Type",contentType+"; charset=\""+charset+"\";\r\n\tboundary=\""+boundary+"\"");
+		}else{
+			setHeader("Content-Type",contentType+"; charset=\""+charset+"\"");
+		}
+
+		if( !subPart ){
+			setHeader("MIME-Version","1.0");
+			setHeader("X-Mailer","haXe mailer");
+		}
+
+		var ret = new StringBuf();
+		
+		// copy headers
+		var myHeaders = new Hash();
+		for( k in headers.keys() ){
+			myHeaders.set(k,headers.get(k));
+		}
+		
+		// Put standard headers
+		for( p in headerOrder ){
+			if( myHeaders.exists(p) ){
+				for( s in myHeaders.get(p) )
+					ret.add(Tools.formatHeader(p,s,charset));
+				myHeaders.remove(p);
+			}
+		}
+		
+		// Put other headers
+		for( k in myHeaders.keys() ){
+			for( s in myHeaders.get(k) )
+				ret.add(Tools.formatHeader(k,s,charset));
+		}
+
+		ret.add("\r\n");
+
+		// Add content
+		if( content.length > 0 ){
+			switch( getHeader("Content-Transfer-Encoding") ){
+				case "base64":
+					ret.add( Tools.encodeBase64(content,"\r\n") + "\r\n" );
+				case "quoted-printable":
+					ret.add( Tools.encodeQuotedPrintable(content,"\r\n") + "\r\n" );
+				default:
+					ret.add( content + "\r\n" );
+			}
+		}
+		
+		// Add parts
+		if( parts.length > 0 ){
+			var pcp = new List<MetaPart<T>>();
+			for( p in parts ){
+				pcp.add(cast p);
+			}
+			
+			if( contentType == "multipart/alternative" ){
+				// text/plain first
+				for( v in pcp ){
+					if( v.contentType == "text/plain" ){
+						ret.add( "--" + boundary + "\r\n" + v.get() );
+						pcp.remove(v);
+					}
+				}
+				
+				// then text/html
+				for( v in pcp ){
+					if( v.contentType == "text/html" ){
+						ret.add( "--" + boundary + "\r\n" + v.get() );
+						pcp.remove(v);
+					}
+				}
+			}
+
+			for( v in pcp ){
+				ret.add( "--" + boundary + "\r\n" + v.get() );
+			}
+			ret.add("--" + boundary + "--\r\n");
+			
+		}
+
+		return ret.toString();
+	}
+
+
+	function parse( str:String, ?id : String ){
+		if( str == null )
+			throw "unable to parse null";
+			
+		if( id == null ) subPart = false;
+		this.id = id;
+
+		var arr = Tools.splitLines(str);
+		var head : List<String> = new List();
+
+		var inHead = true;
+		var buf = new StringBuf();
+		
+		for( ln in arr ){
+			if( !inHead ){
+				buf.add( Tools.removeCRLF(ln) );
+				buf.add("\r\n");
+			}else{
+				if( StringTools.trim(ln).length == 0 ){
+					inHead = false;
+					head.add( buf.toString() );
+					buf = new StringBuf();
+				}else{
+					var nbTab = Tools.countInitTab(ln);
+					if( nbTab > 0 ){
+						buf.add( Tools.removeCRLF(ln.substr(nbTab,ln.length-nbTab)) );
+					}else{
+						head.add( buf.toString() );
+						buf = new StringBuf();
+						buf.add( Tools.removeCRLF(ln) );
+					}
+				}
+			}
+		}
+		content = buf.toString();
+		
+		for( ln in head ){
+			if( REG_HEADER.match(ln) ){
+				var name = Tools.formatHeaderTitle(REG_HEADER.matched(1));
+				var value = StringTools.trim(REG_HEADER.matched(2));
+				if( headers.exists(name) ){
+					headers.get(name).push( value );
+				}else{
+					headers.set(name,[value]);
+				}				
+			}
+		}
+
+		var ctype0 = "text";
+		var ctype1 = "plain";
+
+		// parse contentType
+		var hctype = Tools.parseComplexHeader(getHeader("Content-Type"));
+		if( hctype != null ){
+			var t = hctype.value.split("/");
+			ctype0 = StringTools.trim(t[0]).toLowerCase();
+			ctype1 = StringTools.trim(t[1]).toLowerCase();
+
+			if( hctype.params.exists("charset") ){
+				charset = hctype.params.get("charset");
+			}
+
+			if( hctype.params.exists("boundary") ){
+				boundary = hctype.params.get("boundary");
+			}
+
+			if( hctype.params.exists("name") ){
+				name = hctype.params.get("name");
+			}
+		}
+		
+		contentType = ctype0+"/"+ctype1;
+
+		for( k in headers.keys() ){
+			var a = headers.get(k);
+			for( i in 0...a.length ){
+				a[i] = Tools.headerDecode(a[i],charset);
+			}
+		}
+
+		if( ctype0 == "multipart" ){
+			if( boundary == null || boundary.length == 0 ){
+				contentType = "text/plain";
+				ctype0 = "text";
+				ctype1 = "plain";
+			}else{
+				splitContent();
+			}
+		}
+
+		if( headers.exists("Content-Transfer-Encoding") ){
+			var cte = getHeader("Content-Transfer-Encoding").toLowerCase();
+			if( cte == "quoted-printable" ){
+				content = Tools.decodeQuotedPrintable( content );
+			}else if( cte == "base64" ){
+				content = Tools.decodeBase64( content,"\r\n" );
+			}
+		}
+
+		var cdispo = Tools.parseComplexHeader(getHeader("Content-Disposition"));
+		if( cdispo != null && cdispo.params.exists("filename") ){
+			name = cdispo.params.get("filename");
+		}
+	}
+
+	function splitContent(){
+		var bound = Tools.pregQuote(boundary);
+		var regStr = "(.*?)--"+bound+"(.*)--"+bound+"--";
+		var reg = new EReg(regStr,"s");
+
+		if( reg.match( content ) ){
+			content = reg.matched(1);
+
+			var tmp = reg.matched(2).split("--"+boundary);
+
+			var myId = if(id == null || id.length == 0) "" else id + ".";
+			var i = 0;
+			for( str in tmp ){
+				i++;
+				if( REG_CRLF_END.match(str) ){
+					str = str.substr(0,-REG_CRLF_END.matched(1).length);
+				}
+				
+				var p = cast newPart("text/plain");
+				p.parse( StringTools.trim(str), myId+i );
+			}
+		}
+	}
+
+	public function newPart(ctype:String) : T {
+		throw "Part cannot be used directly : newPart need to be overrided";
+		return null;
+	}
+}

+ 57 - 0
std/mtwin/mail/Smtp.hx

@@ -0,0 +1,57 @@
+package mtwin.mail;
+
+import neko.Socket;
+
+class SmtpException extends mtwin.mail.Exception {
+}
+
+class Smtp {
+
+	public static function send( host : String, from : String, to : String, data : String ){
+		var cnx = new Socket();
+		
+		try {
+			cnx.connect(Socket.resolve(host),25);
+		}catch( e : Dynamic ){
+			throw new SmtpException("SMTP connection failed: "+e);
+		}
+		
+		// get server init line
+		cnx.readLine();
+
+		cnx.write( "MAIL FROM:<" + from + ">\r\n" );
+		var ret = StringTools.trim(cnx.readLine());
+		if( ret.substr(0,3) != "250" ){
+			cnx.close();
+			throw new SmtpException("SMTP error on FROM : " + ret);
+		}
+
+		cnx.write( "RCPT TO:<" + to + ">\r\n" );
+		ret = StringTools.trim(cnx.readLine());
+		if( ret.substr(0,3) != "250" ){
+			cnx.close();
+			throw new SmtpException("SMTP error on RCPT : " + ret);
+		}
+
+		cnx.write( "DATA\r\n" );
+		ret = StringTools.trim(cnx.readLine());
+		if( ret.substr(0,3) != "354" ){
+			cnx.close();
+			throw new SmtpException("SMTP error on DATA : " + ret);
+		}
+
+		if( data.substr(data.length -2,2) != "\r\n" ) 
+			data += "\r\n";
+
+		cnx.write( data + ".\r\n" );
+		ret = StringTools.trim(cnx.readLine());
+		if( ret.substr(0,3) != "250" ){
+			cnx.close();
+			throw new SmtpException("SMTP error on mail content: " + ret);
+		}
+
+		cnx.write( "QUIT\r\n" );
+		cnx.close();
+	}
+	
+}

+ 279 - 0
std/mtwin/mail/Tools.hx

@@ -0,0 +1,279 @@
+package mtwin.mail;
+
+class Tools {
+
+	static var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+	static var HEXA = "0123456789ABCDEF";
+
+
+	static var REG_HEADER_DECODE = ~/^(.*?)=\?([^\?]+)\?(Q|B)\?([^?]*)\?=(.*?)$/i;
+	static var REG_QP_LB = ~/=\\r?\\n/;
+	static var REG_QP = ~/=([A-Fa-f0-9]{1,2})/;
+	static var REG_START_TAB = ~/^(\t| )+/;
+	
+	public static function chunkSplit( str:String, length:Int, sep:String ){
+		var ret = "";
+		while( str.length > length ){
+			ret += str.substr(0,length) + sep;
+			str = str.substr(length,str.length - length);
+		}
+		return ret + str;
+	}
+
+	public static function splitLines( str : String ) : Array<String> {
+		var ret = str.split("\n");
+		for( i in 0...ret.length ){
+			var l = ret[i];
+			if( l.substr(-1,1) == "\r" ){
+				ret[i] = l.substr(0,-1);
+			}
+		}
+		return ret;
+	}
+
+	public static function encodeBase64( content : String , crlf : String ){
+		return StringTools.rtrim(chunkSplit(StringTools.baseEncode( content, BASE64 ), 76, crlf)) + "==";
+	}
+
+	public static function decodeBase64( content : String, crlf ){
+		return StringTools.baseDecode( StringTools.replace(StringTools.replace(content,crlf,""),"=",""), BASE64 );
+	}
+
+	public static function encodeQuotedPrintable( content : String, crlf : String ) : String{
+		var rs = new List();
+		var lines = splitLines( content );
+		
+		for( ln in lines ){
+			var len = ln.length;
+			var line = "";
+			for( i in 0...len ){
+				var c = ln.charAt(i);
+				var o = c.charCodeAt(0);
+				if( o == 9 ){
+				}else if( o < 16 ){
+					c = "=0" + StringTools.baseEncode(c,HEXA);
+				}else if( o == 61 || o < 32 || o > 126 ){
+					c = "=" + StringTools.baseEncode(c,HEXA);
+				}
+
+				// space at the end of line
+				if( i == len - 1 ){
+					if( o == 32 ){
+						c = "=20";
+					}else if( o == 9 ){
+						c = "=09";
+					}
+				}
+
+				// soft line breaks
+				var ll = line.length;
+				var cl = c.length;
+				if( ll + cl >= 76 && (i != len -1 || ll + cl != 76) ){
+					rs.add(line + "=");
+					line = "";
+				}
+				line += c;
+			}
+			rs.add(line);
+		}
+
+		return rs.join(crlf);
+	}
+
+	public static function decodeQuotedPrintable( str : String ){
+		str = ~/=\r?\n/g.replace(str,"");
+		var a = str.split("=");
+		var first = true;
+		var ret = new StringBuf();
+		for( t in a ){
+			if( first ){
+				first = false;
+				ret.add(t);
+			}else{
+				ret.add(StringTools.baseDecode(t.substr(0,2).toUpperCase(),HEXA) + t.substr(2,t.length - 2));
+			}			
+		}
+		return ret.toString();
+	}
+
+	// TODO Protect address in "non ascii chars" <[email protected]>
+	public static function headerQpEncode( ostr : String, initSize : Int, charset : String ){
+		var str = removeCRLF(ostr);
+		
+		var csl = charset.length;
+		var len = str.length;
+		var quotedStr : List<String> = new List();
+		var line = new StringBuf();
+		var llen = 0;
+		var useQuoted = false;
+		for( i in 0...len ){
+			var c = str.charAt(i);
+			var o = c.charCodeAt(0);
+
+			if( o == 9 ){
+			}else if( o < 16 ){
+				useQuoted = true;
+				c = "=0" + StringTools.baseEncode(c,HEXA);
+			}else if( o == 61 || o == 58 || o == 63 || o == 95 || o == 34 ){
+				c = "=" + StringTools.baseEncode(c,HEXA);
+			}else if( o < 32 || o > 126 ){
+				useQuoted = true;
+				c = "=" + StringTools.baseEncode(c,HEXA);
+			}else if( o == 32 ){
+				c = "_";
+			}
+
+			// max line length = 76 - 17 ( =?iso-8859-1?Q?...?= ) => 59 - initSize
+			var max : Int;
+			if( quotedStr.length == 0 ){
+				max = 69 - csl - initSize;
+			}else{
+				max = 69 - csl;
+			}
+			var clen = c.length;
+			if( llen + clen >= max ){
+				quotedStr.add(line.toString());
+				line = new StringBuf();
+				llen = 0;
+			}
+			line.add(c);
+			llen += clen;
+		}
+		quotedStr.add(line.toString());
+
+		if( !useQuoted ){
+			return ostr;
+		}else{
+			return "=?"+charset+"?Q?"+quotedStr.join("?=\r\n\t=?"+charset+"?Q?")+"?=";
+		}
+	}
+
+	public static function headerDecode( str : String, charsetOut : String ){
+		while( REG_HEADER_DECODE.match(str) ){
+			var charset = StringTools.trim(REG_HEADER_DECODE.matched(2).toLowerCase());
+			var encoding = StringTools.trim(REG_HEADER_DECODE.matched(3).toLowerCase());
+			var encoded = StringTools.trim(REG_HEADER_DECODE.matched(4));
+
+			var start = REG_HEADER_DECODE.matched(1);
+			var end = REG_HEADER_DECODE.matched(5);
+
+			if( encoding == "q" ){
+				encoded = decodeQuotedPrintable(StringTools.replace(encoded,"_"," "));
+			}else if( encoding == "b" ){
+				encoded = decodeBase64(encoded,"\r\n");
+			}else{
+				throw "mtwin.mail.MultiPart.headerDecode: Unknow transfer-encoding: "+encoding;
+			}
+
+			charsetOut = charsetOut.toLowerCase();
+			if( charsetOut != "utf-8" && charset == "utf-8" ){
+				encoded =  neko.Utf8.decode( encoded );
+			}else if( charset != "utf-8" && charsetOut == "utf-8" ){
+				encoded =  neko.Utf8.encode( encoded );
+			}
+
+			str = start + encoded + end;
+		}
+
+		return str;
+	}
+
+	public static function removeCRLF( str ){
+		return StringTools.replace(StringTools.replace(str,"\n",""),"\r","");
+	}
+
+	public static function formatHeaderTitle( str : String ) : String {
+		str = StringTools.trim( str );
+		if( str.toLowerCase() == "mime-version" ) return "MIME-Version";
+
+		var arr = str.split("-");
+		for( i in 0...arr.length ){
+			var t = arr[i];
+			arr[i] = t.substr(0,1).toUpperCase()+t.substr(1,t.length-1).toLowerCase();
+		}
+		return arr.join("-");
+	}
+
+	public static function countInitTab( str : String ) : Int {
+		if( REG_START_TAB.match(str) ){
+			return REG_START_TAB.matched(0).length;
+		}else{
+			return 0;
+		}
+	}
+
+	public static function randomEight(){
+		var s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+		var ret = "";
+		for( i in 0...8 ){
+			ret += s.charAt(Std.random(s.length));
+		}
+		return ret;
+	}
+
+	public static function pregQuote( str : String ){
+		str = StringTools.replace(str,"\\","\\\\");
+		str = StringTools.replace(str,".","\\.");
+		str = StringTools.replace(str,"+","\\+");
+		str = StringTools.replace(str,"*","\\*");
+		str = StringTools.replace(str,"?","\\?");
+		str = StringTools.replace(str,"^","\\^");
+		str = StringTools.replace(str,")","\\)");
+		str = StringTools.replace(str,"(","\\(");
+		str = StringTools.replace(str,"[","\\[");
+		str = StringTools.replace(str,"]","\\]");
+		str = StringTools.replace(str,"{","\\{");
+		str = StringTools.replace(str,"}","\\}");
+		str = StringTools.replace(str,"=","\\=");
+		str = StringTools.replace(str,"!","\\!");
+		str = StringTools.replace(str,"<","\\<");
+		str = StringTools.replace(str,">","\\>");
+		str = StringTools.replace(str,"|","\\|");
+		str = StringTools.replace(str,":","\\:");
+		str = StringTools.replace(str,"$","\\$");
+		str = StringTools.replace(str,"/","\\/");
+			
+		return str;
+	}
+
+	public static function formatHeader( name : String, content : String, charset : String ){
+		return name+": "+headerQpEncode(content,name.length,charset)+"\r\n";
+	}
+
+	static var REG_MHEADER = ~/^([^;]+)(.*?)$/;
+	static var REG_PARAM1 = ~/^; *([a-zA-Z]+)="(([^"]|\\")+)"/;
+	static var REG_PARAM2 = ~/^; *([a-zA-Z]+)=([^;]+)/;
+	public static function parseComplexHeader( h : String ){
+		if( h == null ) return null;
+
+		var ret = {value: null, params: new Hash()};
+		if( REG_MHEADER.match(h) ){
+			ret.value = StringTools.trim( REG_MHEADER.matched(1) );
+
+			var params = REG_MHEADER.matched(2);
+			while( params.length > 0 ){
+				params = StringTools.ltrim( params );
+
+				if( REG_PARAM1.match( params ) ){
+					var k = StringTools.trim(REG_PARAM1.matched(1)).toLowerCase();
+					var v = REG_PARAM1.matched(2);
+					ret.params.set( k, v ); 
+					params = REG_PARAM1.matchedRight();
+				}else if( REG_PARAM2.match( params ) ){
+					var k = StringTools.trim(REG_PARAM2.matched(1)).toLowerCase();
+					var v = StringTools.trim(REG_PARAM2.matched(2));
+					ret.params.set( k, v );
+					params = REG_PARAM2.matchedRight();
+				}else{
+					break;
+				}
+			}
+		}else{
+			ret.value = h;
+		}
+		return ret;
+
+	}
+
+}