1 module appbase.gateway.smtp.message;
2 
3 import std.format;
4 import std.uuid : randomUUID;
5 import std.base64 : Base64;
6 
7 import appbase.gateway.smtp.attachment;
8 
9 /++
10   Struct that holds name and address of a person holding e-mail box and is capable of sending messages.
11  +/
12 struct Recipient {
13 	string address;
14 	string name;
15 }
16 
17 /++
18   Implements SmtpMessage compositor.
19   Allows to get string representation of message and also send it via SmtpClient.
20   SmtpMessage is used by `SmtpClient.send` high-level method in order to compose
21   and send mail via SMTP.
22  +/
23 struct SmtpMessage {
24 	static string boundary;        // Parts delmiter in multipart message
25 	Recipient sender;              // Specifies name/address of a sender
26 	Recipient[] recipients;        // Specifies names/adresses of recipients
27 	string subject;                // Message subject
28 	string message;                // Message text (body)
29 	string replyTo;                // Messages chains maked (reply-to:)
30 	SmtpAttachment[] attachments;  // Attachments to message
31 
32 	/++
33 	  Initializes boundary for parts in multipart/mixed message type.
34 		Boundary is a random sequence of chars that must divide message
35 		into parts: message, and attachments.
36 	 +/
37 	static this() {
38 		boundary = randomUUID().toString();
39 	}
40 
41 	/++
42 	  Add attachments to the `SmtpMessage`.
43 	 +/
44 	void attach(SmtpAttachment[] a...) {
45 		attachments ~= a;
46 	}
47 
48 	/++
49 	  Builds string representation for a list of copies-to-send over SMTP.
50 	 +/
51 	private string cc() const {
52 		string tCc = "Cc:\"%s\" <%s>\r\n";
53 		string cc = "";
54 		if (recipients.length > 1) {
55 			foreach(recipient; recipients) {
56 				cc ~= format(tCc, recipient.name, recipient.address);
57 			}
58 		} else {
59 			cc = "";
60 		}
61 		return cc;
62 	}
63 
64 	/++
65 	  Builds message representation in case we have multipart/mixed MIME-type
66 		of the message to send.
67 	 +/
68 	private string messageWithAttachments() const {
69 		const string crlf = "\r\n";
70 		return "Content-Type: text/html; charset=utf-8" ~ crlf
71             ~ "Content-Transfer-Encoding: base64" ~ crlf
72 			~ crlf
73 			~ cast(string) Base64.encode(cast(ubyte[]) message) ~ crlf
74 			~ crlf ~ "--" ~ SmtpMessage.boundary ~ crlf;
75 	}
76 
77 	/++
78 	 Partly converts attachments to string for SMTP protocol representation
79 	 +/
80 	private string attachmentsToString() const {
81 		string result = "";
82 		foreach(ref a; attachments) {
83 			result ~= a.toString(boundary);
84 		}
85 		return (result.length ? result[0..$ - 2] : "") ~ "\r\n";
86 	}
87 
88 	/++
89 	  This method converts SmtpMessage struct to string representation.
90 		This string representation is a ready-to-send representation for
91 		SMTP protocol.
92 	 +/
93 	string toString() const {
94 		const string tFrom      = "From: \"%s\" <%s>\r\n";
95 		const string tTo        = "To: \"%s\" <%s>\r\n";
96 		const string tSubject   = "Subject: %s\r\n";
97 		const string mime       = "MIME-Version: 1.0\r\n";
98 		const string tMultipart = format("Content-Type: multipart/mixed; boundary=\"%s\"\r\n", SmtpMessage.boundary);
99 		const string tReplyTo   = "Reply-To:%s\r\n";
100 		const string crlf       = "\r\n";
101 
102 		if (!attachments.length) {
103 			return format(tFrom, sender.name, sender.address)
104    			 	~ format(tTo, recipients[0].name, recipients[0].address)
105 				~ cc()
106 				~ format(tSubject, buildBase64Msg(subject))
107 				~ mime
108 				~ tMultipart
109 				~ format(tReplyTo, replyTo) ~ crlf
110 				~ "--" ~ boundary ~ crlf
111 				~ messageWithAttachments() ~ crlf;
112 		} else {
113 			return format(tFrom, sender.name, sender.address)
114 				~ format(tTo, recipients[0].name, recipients[0].address)
115 				~ cc()
116 				~ format(tSubject, buildBase64Msg(subject))
117 				~ mime
118 				~ tMultipart
119 				~ format(tReplyTo, replyTo) ~ crlf
120 				~ "--" ~ boundary ~ crlf
121 				~ messageWithAttachments()
122 				~ attachmentsToString();
123 		}
124 	}
125 
126 	private string buildBase64Msg(const string msg) const {
127 		return "=?UTF-8?B?" ~ cast(string) Base64.encode(cast(ubyte[]) msg) ~ "?=";
128 	}
129 }