1 module mail.msg; 2 3 public import mail.headers : Headers; 4 5 import mail.utils; 6 7 import std.algorithm; 8 import std.array : replace; 9 import std.conv : to; 10 import std.base64 : Base64; 11 import std.uri : decode; 12 import std.string; 13 import std.uuid : randomUUID; 14 15 struct Msg 16 { 17 Headers headers; 18 string data; 19 Msg[] parts; 20 ubyte[] rawData; 21 22 @property string plain() const 23 { 24 string content; 25 if (data.length) 26 { 27 content = data; 28 } 29 else 30 { 31 foreach (ref m; parts) 32 { 33 if (!m.headers.all("content-type").length 34 || m.headers["content-type"].toLower.startsWith("text/plain")) 35 { 36 content = m.data; 37 break; 38 } 39 } 40 } 41 return content; 42 } 43 44 void toString(scope void delegate(const(char)[]) sink) const 45 { 46 Headers hM = Headers(headers); 47 Headers hS; 48 49 auto boundary = randomUUID.to!string; 50 51 void _pack(char[] d) 52 { 53 d = Base64.encode(cast(ubyte[]) d); 54 while (d.length) 55 { 56 auto m = min(76, d.length); 57 sink(d[0 .. m] ~ "\r\n"); 58 if (m == d.length) 59 break; 60 d = d[m .. $]; 61 } 62 } 63 64 if (parts.length) 65 { 66 hS["content-type"] = hM.get("content-type", "text/plain"); 67 hM["content-type"] = format("multipart/mixed; boundary=\"%s\"", boundary).dup; 68 sink(hM.to!string); 69 sink("--" ~ boundary ~ "\r\n"); 70 hS["content-transfer-encoding"] = "base64"; 71 72 sink(hS.to!string); 73 } 74 else 75 { 76 hM["content-transfer-encoding"] = "base64"; 77 sink(hM.to!string); 78 } 79 80 _pack(data.dup); 81 82 foreach (i; parts) 83 { 84 sink("--" ~ boundary ~ "\r\n"); 85 sink(i.to!string); 86 } 87 if (parts.length) 88 { 89 sink("--" ~ boundary ~ "--\r\n"); 90 } 91 } 92 93 static Msg parse(ubyte[] srcData) 94 { 95 Msg m; 96 auto tmp = srcData.findSplit(['\r', '\n', '\r', '\n']); 97 m.headers.parse(cast(string) tmp[0]); 98 auto data = tmp[2]; 99 auto ct = m.headers.all("content-type").length ? m.headers["content-type"] : ""; 100 auto enc = m.headers.all("content-transfer-encoding").length 101 ? m.headers["content-transfer-encoding"].toLower : ""; 102 103 if ((cast(string) data).strip.empty) 104 return m; 105 // Transfer encoding 106 switch (enc) 107 { 108 case "quoted-printable": 109 data = data.removeAll(['=', '\r', '\n']); 110 data = data.fromPercentEncoding('='); 111 break; 112 case "base64": 113 data = Base64.decode(data.removeAll('\r').removeAll('\n')); 114 break; 115 default: 116 } 117 118 if (!ct.length) 119 { 120 m.data = (cast(char[]) data).to!string; 121 } 122 else if (ct.toLower.startsWith("multipart/related") 123 || ct.toLower.startsWith("multipart/alternative") 124 || ct.toLower.startsWith("multipart/mixed")) 125 { 126 auto boundary = m.headers["content-type"].findSplit("boundary=")[2]; 127 if (boundary[0] == '"') 128 { 129 boundary = boundary[1 .. $ - 1]; 130 } 131 foreach (part; (cast(string) data).split("--" ~ boundary)[1 .. $]) 132 { 133 if (part == "--") 134 break; 135 m.parts ~= Msg.parse(cast(ubyte[]) part.strip); 136 } 137 } 138 else 139 { 140 auto charset = m.headers["content-type"].findSplitAfter("charset=")[1].toUpper; 141 if (charset[0] == '"') 142 { 143 charset = charset[1 .. $ - 1]; 144 } 145 switch (charset) 146 { 147 case "UTF-8": 148 m.data = to!string(cast(char[]) data); 149 break; 150 default: 151 data = cast(ubyte[]) recode(charset, "UTF-8", cast(char[]) data); 152 m.data = to!string(cast(char[]) data); 153 } 154 } 155 return m; 156 } 157 }