Skip to content

Commit 0a9dec6

Browse files
authored
Attachments (#48)
* add body and attachment constructor * improve formatting * add tests * formatting * linting * rename `get_message` to `get_mime_message` * add dispatch to `get_mime_msg` only with msg * make it "attachments" (plural) * update README * mention and link rfc5322 * fix date in `get_body` to use locale current time * fix From field * fix From field * fix From field * fix Date field formatting * add comment about bcc * typos * discard extra test file
1 parent ef324e9 commit 0a9dec6

File tree

7 files changed

+424
-13
lines changed

7 files changed

+424
-13
lines changed

Project.toml

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
name = "SMTPClient"
22
uuid = "c35d69d1-b747-5018-a192-25bc5e63c83d"
33
authors = ["Avik Sengupta <[email protected]>", "Iblis Lin <[email protected]>"]
4-
version = "0.6.0"
4+
version = "0.6.1"
55

66
[deps]
7+
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
8+
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
79
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
810
LibCURL = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
911
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
1012

11-
[extras]
12-
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
13-
1413
[compat]
15-
julia = "1.3"
1614
LibCURL = "0.6"
15+
julia = "1.3"
16+
17+
[extras]
18+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1719

1820
[targets]
1921
test = ["Test"]

README.md

+59-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
[![Pkg Eval](https://juliahub.com/docs/SMTPClient/pkgeval.svg)](https://juliahub.com/ui/Packages/SMTPClient/Bx8Fn/)
66
[![Dependents](https://juliahub.com/docs/SMTPClient/deps.svg)](https://juliahub.com/ui/Packages/SMTPClient/Bx8Fn/?t=2)
77

8-
98
A [CURL](curl.haxx.se) based SMTP client with fairly low level API.
109
It is useful for sending emails from within Julia code.
1110
Depends on [LibCURL.jl](https://github.com/JuliaWeb/LibCURL.jl/).
@@ -51,7 +50,8 @@ resp = send(url, rcpt, from, body, opt)
5150
```
5251

5352
### Example with HTML Formatting
54-
```
53+
54+
```julia
5555
body = "Subject: A simple test\r\n"*
5656
"Mime-Version: 1.0;\r\n"*
5757
"Content-Type: text/html;\r\n"*
@@ -65,13 +65,64 @@ body = "Subject: A simple test\r\n"*
6565
</html>\r\n"""
6666
```
6767

68+
### Function to construct the IOBuffer body and for adding attachments
69+
70+
A new function `get_body()` is available to facilitate constructing the IOBuffer for the body of the message and for adding attachments.
71+
72+
The function takes four required arguments: the `to` and `from` email addresses, a `subject` string, and a `msg` string. The `to` argument is a vector of strings, containing one or more email addresses. The `msg` string can be a regular string with the contents of the message or a string in MIME format, following the [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) specifications.
73+
74+
There are also the optional keyword arguments `cc`, `replyto` and `attachments`. The argument `cc` should be a vector of strings, containing one or more email addresses, while `replyto` is a string expected to contain a single argument, just like `from`. The `attachments` argument should be a list of filenames to be attached to the message.
75+
76+
The attachments are encoded using `Base64.base64encode` and included in the IOBuffer variable returned by the function. The function `get_body()` takes care of identifying which type of attachments are to be included (from the filename extensions) and to properly add them according to the MIME specifications.
77+
78+
In case an attachment is to be added, the `msg` argument must be formatted according to the MIME specifications. In order to help with that, another function, `get_mime_msg(message)`, is provided, which takes the provided message and returns the message with the proper MIME specifications. By default, it assumes plain text with UTF-8 encoding, but plain text with different encodings or HTML text can also be given can be given (see [src/user.jl#L36](src/user.jl#L35) for the arguments).
79+
80+
As for blind carbon copy (Bcc), it is implicitly handled by `send()`. Every recipient in `send()` which is not included in `body` is treated as a Bcc.
81+
82+
Here are two examples:
83+
84+
```julia
85+
using SMTPClient
86+
87+
opt = SendOptions(
88+
isSSL = true,
89+
username = "[email protected]",
90+
passwd = "yourgmailpassword"
91+
)
92+
93+
url = "smtps://smtp.gmail.com:465"
94+
95+
message = "Don't forget to check out SMTPClient.jl"
96+
subject = "SMPTClient.jl"
97+
98+
99+
100+
bcc = ["<[email protected]>"]
101+
from = "You <[email protected]>"
102+
replyto = "<[email protected]>"
103+
104+
body = get_body(to, from, subject, message; cc, replyto)
105+
106+
rcpt = vcat(to, cc, bcc)
107+
resp = send(url, rcpt, from, body, opt)
108+
```
109+
110+
```julia
111+
message = "Check out this cool logo!"
112+
subject = "Julia logo"
113+
attachments = ["julia_logo_color.svg"]
114+
115+
mime_msg = get_mime_msg(message)
116+
117+
body = get_body(to, from, subject, mime_msg; attachments)
118+
```
68119

69120
### Gmail Notes
70121

71122
Due to the security policy of Gmail,
72123
you need to "allow less secure apps into your account":
73124

74-
- https://myaccount.google.com/lesssecureapps
125+
- <https://myaccount.google.com/lesssecureapps>
75126

76127
The URL for gmail can be either `smtps://smtp.gmail.com:465` or `smtp://smtp.gmail.com:587`.
77128
(Note the extra `s` in the former.)
@@ -98,12 +149,12 @@ send(url, to-addresses, from-address, message-body, options)
98149
```
99150

100151
Send an email.
101-
* `url` should be of the form `smtp://server:port` or `smtps://...`.
102-
* `to-address` is a vector of `String`.
103-
* `from-address` is a `String`. All addresses must be enclosed in angle brackets.
104-
* `message-body` must be a RFC5322 formatted message body provided via an `IO`.
105-
* `options` is an object of type `SendOptions`. It contains authentication information, as well as the option of whether the server requires TLS.
106152

153+
* `url` should be of the form `smtp://server:port` or `smtps://...`.
154+
* `to-address` is a vector of `String`.
155+
* `from-address` is a `String`. All addresses must be enclosed in angle brackets.
156+
* `message-body` must be a RFC5322 formatted message body provided via an `IO`.
157+
* `options` is an object of type `SendOptions`. It contains authentication information, as well as the option of whether the server requires TLS.
107158

108159
```julia
109160
SendOptions(; isSSL = false, verbose = false, username = "", passwd = "")

src/SMTPClient.jl

+5
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ module SMTPClient
22

33
using Distributed
44
using LibCURL
5+
using Dates
6+
using Base64
57

68
import Base: convert
79
import Sockets: send
810

911
export SendOptions, SendResponse, send
12+
export get_body, get_mime_msg
1013

1114
include("utils.jl")
1215
include("types.jl")
1316
include("cbs.jl") # callbacks
1417
include("mail.jl")
18+
include("mime_types.jl")
19+
include("user.jl")
1520

1621
##############################
1722
# Module init/cleanup

src/mime_types.jl

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
mime_types = Dict(
2+
[
3+
"abs" => "audio/x-mpeg"
4+
"ai" => "application/postscript"
5+
"aif" => "audio/x-aiff"
6+
"aifc" => "audio/x-aiff"
7+
"aiff" => "audio/x-aiff"
8+
"aim" => "application/x-aim"
9+
"art" => "image/x-jg"
10+
"asf" => "video/x-ms-asf"
11+
"asx" => "video/x-ms-asf"
12+
"au" => "audio/basic"
13+
"avi" => "video/x-msvideo"
14+
"avx" => "video/x-rad-screenplay"
15+
"bcpio" => "application/x-bcpio"
16+
"bin" => "application/octet-stream"
17+
"bmp" => "image/bmp"
18+
"body" => "text/html"
19+
"cdf" => "application/x-cdf"
20+
"cer" => "application/x-x509-ca-cert"
21+
"class" => "application/java"
22+
"cpio" => "application/x-cpio"
23+
"csh" => "application/x-csh"
24+
"css" => "text/css"
25+
"dib" => "image/bmp"
26+
"doc" => "application/msword"
27+
"dtd" => "text/plain"
28+
"dv" => "video/x-dv"
29+
"dvi" => "application/x-dvi"
30+
"eps" => "application/postscript"
31+
"etx" => "text/x-setext"
32+
"exe" => "application/octet-stream"
33+
"gif" => "image/gif"
34+
"gtar" => "application/x-gtar"
35+
"gz" => "application/x-gzip"
36+
"hdf" => "application/x-hdf"
37+
"hqx" => "application/mac-binhex40"
38+
"htc" => "text/x-component"
39+
"htm" => "text/html"
40+
"html" => "text/html"
41+
"ief" => "image/ief"
42+
"jad" => "text/vnd.sun.j2me.app-descriptor"
43+
"jar" => "application/octet-stream"
44+
"java" => "text/plain"
45+
"jnlp" => "application/x-java-jnlp-file"
46+
"jpe" => "image/jpeg"
47+
"jpeg" => "image/jpeg"
48+
"jpg" => "image/jpeg"
49+
"js" => "text/javascript"
50+
"kar" => "audio/x-midi"
51+
"latex" => "application/x-latex"
52+
"m3u" => "audio/x-mpegurl"
53+
"mac" => "image/x-macpaint"
54+
"man" => "application/x-troff-man"
55+
"me" => "application/x-troff-me"
56+
"mid" => "audio/x-midi"
57+
"midi" => "audio/x-midi"
58+
"mif" => "application/x-mif"
59+
"mov" => "video/quicktime"
60+
"movie" => "video/x-sgi-movie"
61+
"mp1" => "audio/x-mpeg"
62+
"mp2" => "audio/x-mpeg"
63+
"mp3" => "audio/x-mpeg"
64+
"mpa" => "audio/x-mpeg"
65+
"mpe" => "video/mpeg"
66+
"mpeg" => "video/mpeg"
67+
"mpega" => "audio/x-mpeg"
68+
"mpg" => "video/mpeg"
69+
"mpv2" => "video/mpeg2"
70+
"ms" => "application/x-wais-source"
71+
"nc" => "application/x-netcdf"
72+
"oda" => "application/oda"
73+
"pbm" => "image/x-portable-bitmap"
74+
"pct" => "image/pict"
75+
"pdf" => "application/pdf"
76+
"pgm" => "image/x-portable-graymap"
77+
"pic" => "image/pict"
78+
"pict" => "image/pict"
79+
"pls" => "audio/x-scpls"
80+
"png" => "image/png"
81+
"pnm" => "image/x-portable-anymap"
82+
"pnt" => "image/x-macpaint"
83+
"ppm" => "image/x-portable-pixmap"
84+
"ps" => "application/postscript"
85+
"psd" => "image/x-photoshop"
86+
"qt" => "video/quicktime"
87+
"qti" => "image/x-quicktime"
88+
"qtif" => "image/x-quicktime"
89+
"ras" => "image/x-cmu-raster"
90+
"rgb" => "image/x-rgb"
91+
"rm" => "application/vnd.rn-realmedia"
92+
"roff" => "application/x-troff"
93+
"rtf" => "application/rtf"
94+
"rtx" => "text/richtext"
95+
"sh" => "application/x-sh"
96+
"shar" => "application/x-shar"
97+
"smf" => "audio/x-midi"
98+
"snd" => "audio/basic"
99+
"src" => "application/x-wais-source"
100+
"sv4cpio" => "application/x-sv4cpio"
101+
"sv4crc" => "application/x-sv4crc"
102+
"swf" => "application/x-shockwave-flash"
103+
"t" => "application/x-troff"
104+
"tar" => "application/x-tar"
105+
"tcl" => "application/x-tcl"
106+
"tex" => "application/x-tex"
107+
"texi" => "application/x-texinfo"
108+
"texinfo" => "application/x-texinfo"
109+
"tif" => "image/tiff"
110+
"tiff" => "image/tiff"
111+
"tr" => "application/x-troff"
112+
"tsv" => "text/tab-separated-values"
113+
"txt" => "text/plain"
114+
"ulw" => "audio/basic"
115+
"ustar" => "application/x-ustar"
116+
"xbm" => "image/x-xbitmap"
117+
"xpm" => "image/x-xpixmap"
118+
"xwd" => "image/x-xwindowdump"
119+
"wav" => "audio/x-wav"
120+
"wbmp" => "image/vnd.wap.wbmp"
121+
"wml" => "text/vnd.wap.wml"
122+
"wmlc" => "application/vnd.wap.wmlc"
123+
"wmls" => "text/vnd.wap.wmlscript"
124+
"wmlscriptc" => "application/vnd.wap.wmlscriptc"
125+
"wrl" => "x-world/x-vrml"
126+
"Z" => "application/x-compress"
127+
"z" => "application/x-compress"
128+
"zip" => "application/zip"
129+
]
130+
)

src/user.jl

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
function encode_attachment(filename::String, boundary::String)
2+
io = IOBuffer()
3+
iob64_encode = Base64EncodePipe(io)
4+
open(filename, "r") do f
5+
write(iob64_encode, f)
6+
end
7+
close(iob64_encode)
8+
9+
filename_ext = split(filename, '.')[end]
10+
11+
if haskey(mime_types, filename_ext)
12+
content_type = mime_types[filename_ext]
13+
else
14+
content_type = "application/octet-stream"
15+
end
16+
17+
if haskey(mime_types, filename_ext) && startswith(mime_types[filename_ext], "image")
18+
content_disposition = "inline"
19+
else
20+
content_disposition = "attachment"
21+
end
22+
23+
encoded_str =
24+
"--$boundary\r\n" *
25+
"Content-Disposition: $content_disposition;\r\n" *
26+
" filename=$(basename(filename))\r\n" *
27+
"Content-Type: $content_type;\r\n" *
28+
" name=\"$(basename(filename))\"\r\n" *
29+
"Content-Transfer-Encoding: base64\r\n" *
30+
"$(String(take!(io)))\r\n" *
31+
"--$boundary\r\n"
32+
return encoded_str
33+
end
34+
35+
# See https://www.w3.org/Protocols/rfc1341/7_1_Text.html about charset
36+
function get_mime_msg(message::String, ::Val{:plain}, charset::String = "UTF-8")
37+
msg =
38+
"Content-Type: text/plain; charset=\"$charset\"" *
39+
"Content-Transfer-Encoding: quoted-printable\r\n\r\n" *
40+
"$message\r\n"
41+
return msg
42+
end
43+
44+
get_mime_msg(message::String, ::Val{:utf8}) =
45+
get_mime_msg(message, Val(:plain), "UTF-8")
46+
47+
get_mime_msg(message::String, ::Val{:usascii}) =
48+
get_mime_msg(message, Val(:plain), "US-ASCII")
49+
50+
get_mime_msg(message::String) = get_mime_msg(message, Val(:utf8))
51+
52+
function get_mime_msg(message::String, ::Val{:html})
53+
msg =
54+
"Content-Type: text/html;\r\n" *
55+
"Content-Transfer-Encoding: 7bit;\r\n\r\n" *
56+
"\r\n" *
57+
message *
58+
"\r\n"
59+
return msg
60+
end
61+
62+
#Provide the message body as RFC5322 within an IO
63+
64+
function get_body(
65+
to::Vector{String},
66+
from::String,
67+
subject::String,
68+
msg::String;
69+
cc::Vector{String} = String[],
70+
replyto::String = "",
71+
attachments::Vector{String} = String[]
72+
)
73+
74+
boundary = "Julia_SMTPClient-" * join(rand(collect(vcat('0':'9','A':'Z','a':'z')), 40))
75+
76+
tz = mapreduce(
77+
x -> string(x, pad=2), *,
78+
divrem( div( ( now() - now(Dates.UTC) ).value, 60000 ), 60 )
79+
)
80+
date = join([Dates.format(now(), "e, d u yyyy HH:MM:SS", locale="english"), tz], " ")
81+
82+
contents =
83+
"From: $from\r\n" *
84+
"Date: $date\r\n" *
85+
"Subject: $subject\r\n" *
86+
ifelse(length(cc) > 0, "Cc: $(join(cc, ", "))\r\n", "") *
87+
ifelse(length(replyto) > 0, "Reply-To: $replyto\r\n", "") *
88+
"To: $(join(to, ", "))\r\n"
89+
90+
if length(attachments) == 0
91+
contents *=
92+
"MIME-Version: 1.0\r\n" *
93+
"$msg\r\n\r\n"
94+
else
95+
contents *=
96+
"Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n\r\n" *
97+
"MIME-Version: 1.0\r\n" *
98+
"\r\n" *
99+
"This is a message with multiple parts in MIME format.\r\n" *
100+
"--$boundary\r\n" *
101+
"$msg\r\n" *
102+
"--$boundary\r\n" *
103+
"\r\n" *
104+
join(encode_attachment.(attachments, boundary), "\r\n")
105+
end
106+
body = IOBuffer(contents)
107+
return body
108+
end

test/runtests.jl

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Test
2+
import Base64: base64decode
23
using SMTPClient
34

45

0 commit comments

Comments
 (0)