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.contentdisposition;
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 = pipes.stdin.lockingTextWriter;
94       entity.copy(wout);
95       pipes.stdin.close();
96       wait(pipes.pid);
97     }
98   }
99 
100 
101   //---------------------------------------------------------
102   // throws CurlException on failure.
103   
104   void send(string host = "smtp://localhost", string authAccount = null, string authPass = null)
105   {
106     import std.net.curl;
107     auto smtp = SMTP(host);
108     if (authAccount)
109       smtp.setAuthentication(authAccount, authPass);
110 
111     auto entity = build();
112 
113     string[] r;
114     foreach (b; to)
115       r ~= b.address;
116     foreach (b;cc)
117       r ~= b.address;
118     foreach (b;bcc)
119       r ~= b.address;
120     smtp.mailTo(cast(const(char)[][])r);
121     smtp.mailFrom = from.address;
122     smtp.message = entity.asString;
123     smtp.perform();
124   }
125 
126   //---------------------------------------------------------
127   // Constructs the IMF document for the email. The mime
128   // entities themselves handle the serialisation.
129   
130   private MimeEntity build()
131   {
132     MimeEntity entity;
133 
134     if (!attachments.empty)
135     {
136       entity = getMultiPart("mixed");
137       entity.content.bodParts ~= getMessagePart();
138       foreach (a;attachments)
139         entity.content.bodParts ~= getAttachmentPart(a);
140     }
141     else
142       entity = getMessagePart();
143 
144     entity.headers ~= mimeVersion;
145 
146     entity.headers ~= unstructuredHeader("Subject",subject);
147     entity.headers ~= addressHeader("From", [ from ] );
148     if (!to.empty) entity.headers ~= addressHeader("To", to);
149     if (!cc.empty) entity.headers ~= addressHeader("Cc", cc);
150     if (!bcc.empty) entity.headers ~= addressHeader("Bcc", bcc);
151   
152     entity.headers ~= headers;
153     
154     return entity;
155   }
156 
157   //---------------------------------------------------------
158   // Creates an entity for the message, and fills it with the
159   // email message contents.
160 
161   private MimeEntity getMessagePart()
162   {
163     if (!html.empty)
164     {
165       if (!text.empty)
166       {
167         auto entity = getMultiPart("alternate");
168         entity.content.bodParts ~= getHtmlPart();
169         entity.content.bodParts ~= getTextPart();
170         return entity;
171       }
172       else
173         return getHtmlPart();
174     }
175     else
176       return getTextPart();
177   }
178 
179   //---------------------------------------------------------
180   // Creates a mime entity from the text component.
181 
182   private MimeEntity getTextPart()
183   {
184     MimeContentType ct;
185     ct.mimeType = "text/plain";
186     ct.parameters["charset"] = "UTF-8";
187     
188     auto entity = MimeEntity(ct, true);
189     entity.encoding = "8bit";
190 
191     entity.content.bod = cast(ByteArray) text;
192 
193     return entity;
194   }
195 
196   //---------------------------------------------------------
197   // Creates a multipart mime entity of the given subtype.
198 
199   private MimeEntity getMultiPart(string subType)
200   {
201     MimeContentType ct;
202     ct.mimeType = "multipart/"~subType;
203     auto entity = MimeEntity(ct, true);
204     return entity;
205   }
206 
207   //---------------------------------------------------------
208   // Creates a mime entity from the HTML component.
209 
210   private MimeEntity getHtmlPart()
211   {
212     MimeContentType ct;
213     ct.mimeType = "text/html";
214     ct.parameters["charset"] = "UTF-8";
215     
216     auto entity = MimeEntity(ct, true);
217     entity.encoding = "8bit";
218 
219     entity.content.bod = cast(ByteArray) html;
220 
221     return entity;
222   }
223 
224   //---------------------------------------------------------
225   // Creates a mime entity for an attachment.
226 
227   private MimeEntity getAttachmentPart(ref Attachment a)
228   {
229     MimeContentType ct;
230     ct.mimeType = a.mimeType;
231     MimeContentDisposition disp;
232     if (ct.mimeType[0..5] != "image")
233       disp.type = "attachment";
234     disp.parameters["filename"] = a.name;
235     
236     auto entity = MimeEntity(ct, true);
237     entity.headers ~= disp.toMimeHeader(true);
238     entity.encoding = "base64";
239     if (!a.content.empty)
240       entity.content.bod = a.content;
241     else
242       entity.content.bod = cast(ByteArray) read(a.fileName);
243 
244     return entity;
245   }
246 }
247