1 // Written in the D language
2 /*
3  * Code for creating and sending emails.
4  *
5  * Copyright (C) 2014 Jaypha
6  *
7  * Distributed under the Boost Software License, Version 1.0.
8  * (See http://www.boost.org/LICENSE_1_0.txt)
9  *
10  * Authors: Jason den Dulk
11  */
12 
13 module jaypha.inet.email;
14 
15 import jaypha.types;
16 
17 public import jaypha.inet.imf.writing;
18 import jaypha.inet.mime.writing;
19 import jaypha.inet.mime.content_disposition;
20 
21 import std.file;
22 import std.process;
23 import std.array;
24 import std.stdio;
25 import std.range;
26 
27 //----------------------------------------------------------------------------
28 
29 struct Email
30 {
31   struct Attachment
32   {
33     string name;
34     string mimeType;
35     ByteArray content;
36     string fileName;
37   }
38 
39   private MimeHeader[] headers;
40   void addHeader(string name, string fieldBody)
41   { headers ~= unstructuredHeader(name,fieldBody); }
42 
43   string subject;
44 
45   private Mailbox _from;
46   private Mailbox[] _to, _cc, _bcc;
47   @property
48   {
49     Mailbox from() { return _from; }
50     void from(Mailbox v) { _from = v; }
51     void from(string v) { _from = Mailbox(v); }
52 
53     Mailbox[] to() { return _to; }
54     void to(Mailbox[] v) { _to = v; }
55     void to(string[] v) { _to = []; foreach(x;v) _to ~= Mailbox(x); }
56     void to(Mailbox v) { _to = [ v ]; }
57     void to(string v) { _to = [ Mailbox(v) ]; }
58 
59     Mailbox[] cc() { return _cc; }
60     void cc(Mailbox[] v) { _cc = v; }
61     void cc(string[] v) { _cc = []; foreach(x;v) _cc ~= Mailbox(x); }
62     void cc(Mailbox v) { _cc = [ v ]; }
63     void cc(string v) { _cc = [ Mailbox(v) ]; }
64 
65     Mailbox[] bcc() { return _bcc; }
66     void bcc(Mailbox[] v) { _bcc = v; }
67     void bcc(string[] v) { _bcc = []; foreach(x;v) _bcc ~= Mailbox(x); }
68     void bcc(Mailbox v) { _bcc = [ v ]; }
69     void bcc(string v) { _bcc = [ Mailbox(v) ]; }
70   }
71 
72   string text, html;
73 
74   Attachment[] attachments;
75 
76   //---------------------------------------------------------
77 
78   void copy(R) (R range) if (isOutputRange!(R,ByteArray))
79   {
80     auto entity = build();
81     entity.copy(range);
82   }
83 
84   //---------------------------------------------------------
85 
86   version(linux)
87   {
88     void sendmail()
89     {
90       auto entity = build();
91 
92       auto pipes = pipeProcess(["sendmail","-t","-i"]);
93       auto wout = wOut(pipes.stdin);
94       entity.copy(wout);
95       //entity.copy(pipes.stdin);
96       pipes.stdin.close();
97       wait(pipes.pid);
98     }
99   }
100 
101 
102   //---------------------------------------------------------
103   // throws CurlException on failure.
104   
105   void send(string host = "smtp://localhost", string authAccount = null, string authPass = null)
106   {
107     import std.net.curl;
108     auto smtp = SMTP(host);
109     if (authAccount)
110       smtp.setAuthentication(authAccount, authPass);
111 
112     auto entity = build();
113 
114     string[] r;
115     foreach (b; to)
116       r ~= b.address;
117     foreach (b;cc)
118       r ~= b.address;
119     foreach (b;bcc)
120       r ~= b.address;
121     smtp.mailTo(cast(const(char)[][])r);
122     smtp.mailFrom = from.address;
123     smtp.message = entity.asString;
124     smtp.perform();
125   }
126 
127   //---------------------------------------------------------
128   // Constructs the IMF document for the email. The mime
129   // entities themselves handle the serialisation.
130   
131   private MimeEntity build()
132   {
133     MimeEntity entity;
134 
135     if (!attachments.empty)
136     {
137       entity = getMultiPart("mixed");
138       entity.content.bodParts ~= getMessagePart();
139       foreach (a;attachments)
140         entity.content.bodParts ~= getAttachmentPart(a);
141     }
142     else
143       entity = getMessagePart();
144 
145     entity.headers ~= mimeVersion;
146 
147     entity.headers ~= unstructuredHeader("Subject",subject);
148     entity.headers ~= addressHeader("From", [ from ] );
149     if (!to.empty) entity.headers ~= addressHeader("To", to);
150     if (!cc.empty) entity.headers ~= addressHeader("Cc", cc);
151     if (!bcc.empty) entity.headers ~= addressHeader("Bcc", bcc);
152   
153     entity.headers ~= headers;
154     
155     return entity;
156   }
157 
158   //---------------------------------------------------------
159   // Creates an entity for the message, and fills it with the
160   // email message contents.
161 
162   private MimeEntity getMessagePart()
163   {
164     if (!html.empty)
165     {
166       if (!text.empty)
167       {
168         auto entity = getMultiPart("alternate");
169         entity.content.bodParts ~= getHtmlPart();
170         entity.content.bodParts ~= getTextPart();
171         return entity;
172       }
173       else
174         return getHtmlPart();
175     }
176     else
177       return getTextPart();
178   }
179 
180   //---------------------------------------------------------
181   // Creates a mime entity from the text component.
182 
183   private MimeEntity getTextPart()
184   {
185     MimeContentType ct;
186     ct.type = "text/plain";
187     ct.parameters["charset"] = "UTF-8";
188     
189     auto entity = MimeEntity(ct, true);
190     entity.encoding = "8bit";
191 
192     entity.content.bod = cast(ByteArray) text;
193 
194     return entity;
195   }
196 
197   //---------------------------------------------------------
198   // Creates a multipart mime entity of the given subtype.
199 
200   private MimeEntity getMultiPart(string subType)
201   {
202     MimeContentType ct;
203     ct.type = "multipart/"~subType;
204     auto entity = MimeEntity(ct, true);
205     return entity;
206   }
207 
208   //---------------------------------------------------------
209   // Creates a mime entity from the HTML component.
210 
211   private MimeEntity getHtmlPart()
212   {
213     MimeContentType ct;
214     ct.type = "text/html";
215     ct.parameters["charset"] = "UTF-8";
216     
217     auto entity = MimeEntity(ct, true);
218     entity.encoding = "8bit";
219 
220     entity.content.bod = cast(ByteArray) html;
221 
222     return entity;
223   }
224 
225   //---------------------------------------------------------
226   // Creates a mime entity for an attachment.
227 
228   private MimeEntity getAttachmentPart(ref Attachment a)
229   {
230     MimeContentType ct;
231     ct.type = a.mimeType;
232     MimeContentDisposition disp;
233     if (ct.type[0..5] != "image")
234       disp.type = "attachment";
235     disp.parameters["filename"] = a.name;
236     
237     auto entity = MimeEntity(ct, true);
238     entity.headers ~= disp.toMimeHeader(true);
239     entity.encoding = "base64";
240     if (!a.content.empty)
241       entity.content.bod = a.content;
242     else
243       entity.content.bod = cast(ByteArray) read(a.fileName);
244 
245     return entity;
246   }
247 }
248 
249 // Quick and dirty wrapper to make files act as ranges.
250 
251 struct wOut
252 {
253   File file;
254 
255   void put(ByteArray s) { file.rawWrite(s); }
256 }
257