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 }