-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathzig-zerosmtp.zig
More file actions
150 lines (131 loc) · 5.68 KB
/
Copy pathzig-zerosmtp.zig
File metadata and controls
150 lines (131 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// zig-zerosmtp.zig
// Zig 0.14 + libcurl - ZeroSMTP mx.msgwing.com:465 SSL/TLS
// Production-ready | Let's Encrypt | Certificate verification on
//
// Tested against Zig 0.14.1. The language is still moving; an unpinned
// example goes stale quietly, so the CI job pins the same version.
//
// libcurl rather than std.crypto.tls: Zig's standard library has a TLS
// client but no SMTP client, so the protocol would have to be written by
// hand here - and a hand-rolled SMTP conversation in an example is how
// somebody ends up shipping one. The C example makes the same call for
// the same reason.
//
// Debian/Ubuntu apt install libcurl4-openssl-dev
// RHEL/Fedora dnf install libcurl-devel
// Alpine apk add curl-dev
//
// Build: zig build-exe zig-zerosmtp.zig -lc -lcurl
const std = @import("std");
const c = @cImport({
@cInclude("curl/curl.h");
});
const SMTP_URL = "smtps://mx.msgwing.com:465";
/// libcurl asks for the message in chunks, so hand back whatever fits and
/// remember how far we got. Returning 0 signals end of data.
const Upload = struct {
data: []const u8,
offset: usize = 0,
};
fn payloadSource(
ptr: [*c]u8,
size: usize,
nitems: usize,
userdata: ?*anyopaque,
) callconv(.C) usize {
const upload: *Upload = @ptrCast(@alignCast(userdata orelse return 0));
const room = size * nitems;
const remaining = upload.data.len - upload.offset;
if (room == 0 or remaining == 0) return 0;
const chunk = @min(room, remaining);
@memcpy(ptr[0..chunk], upload.data[upload.offset..][0..chunk]);
upload.offset += chunk;
return chunk;
}
/// std.posix.getenv returns a NUL-terminated slice, which is what the C
/// API needs - no copy, no allocator.
fn envOr(name: [:0]const u8, fallback: [:0]const u8) [:0]const u8 {
const value = std.posix.getenv(name) orelse return fallback;
if (value.len == 0) return fallback;
// getenv gives [:0]const u8 on POSIX, so the sentinel is already there.
return @ptrCast(value);
}
pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const username = envOr("ZEROSMTP_USERNAME", "your-username");
const password = envOr("ZEROSMTP_PASSWORD", "your-password");
const from = envOr("ZEROSMTP_FROM", "sender@example.com");
const to = envOr("ZEROSMTP_TO", "recipient@example.com");
const subject = envOr("ZEROSMTP_SUBJECT", "Test Email from ZeroSMTP");
// SMTP wants CRLF line endings, not the bare LF a multiline string
// literal produces - some servers reject or mangle LF-only headers.
const boundary = "boundary_zerosmtp_zig";
const message = try std.fmt.allocPrint(allocator,
"From: {s}\r\n" ++
"To: {s}\r\n" ++
"Subject: {s}\r\n" ++
"MIME-Version: 1.0\r\n" ++
"Content-Type: multipart/alternative; boundary=\"{s}\"\r\n" ++
"\r\n" ++
"--{s}\r\n" ++
"Content-Type: text/plain; charset=\"UTF-8\"\r\n" ++
"\r\n" ++
"Hello from ZeroSMTP! This is plain text.\r\n" ++
"\r\n" ++
"--{s}\r\n" ++
"Content-Type: text/html; charset=\"UTF-8\"\r\n" ++
"\r\n" ++
"<html><body><h1>Hello from ZeroSMTP!</h1>" ++
"<p>This is an HTML email sent via mx.msgwing.com:465</p>" ++
"</body></html>\r\n" ++
"\r\n" ++
"--{s}--\r\n",
.{ from, to, subject, boundary, boundary, boundary, boundary });
defer allocator.free(message);
// The envelope addresses go in angle brackets, unlike the header ones.
const envelope_from = try std.fmt.allocPrintZ(allocator, "<{s}>", .{from});
defer allocator.free(envelope_from);
const envelope_to = try std.fmt.allocPrintZ(allocator, "<{s}>", .{to});
defer allocator.free(envelope_to);
_ = c.curl_global_init(c.CURL_GLOBAL_DEFAULT);
defer c.curl_global_cleanup();
const curl = c.curl_easy_init() orelse {
std.debug.print("Could not initialise libcurl\n", .{});
return 1;
};
defer c.curl_easy_cleanup(curl);
const recipients = c.curl_slist_append(null, envelope_to.ptr) orelse {
std.debug.print("Out of memory building the recipient list\n", .{});
return 1;
};
defer c.curl_slist_free_all(recipients);
var upload = Upload{ .data = message };
_ = c.curl_easy_setopt(curl, c.CURLOPT_URL, SMTP_URL);
_ = c.curl_easy_setopt(curl, c.CURLOPT_USERNAME, username.ptr);
_ = c.curl_easy_setopt(curl, c.CURLOPT_PASSWORD, password.ptr);
_ = c.curl_easy_setopt(curl, c.CURLOPT_MAIL_FROM, envelope_from.ptr);
_ = c.curl_easy_setopt(curl, c.CURLOPT_MAIL_RCPT, recipients);
_ = c.curl_easy_setopt(curl, c.CURLOPT_READFUNCTION, payloadSource);
_ = c.curl_easy_setopt(curl, c.CURLOPT_READDATA, &upload);
_ = c.curl_easy_setopt(curl, c.CURLOPT_UPLOAD, @as(c_long, 1));
_ = c.curl_easy_setopt(curl, c.CURLOPT_TIMEOUT, @as(c_long, 30));
// On by default, set explicitly so nobody "fixes" a certificate error
// later by flipping these to 0. A failure here means the machine's CA
// store cannot validate the server, not that the server is wrong.
_ = c.curl_easy_setopt(curl, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1));
_ = c.curl_easy_setopt(curl, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2));
const result = c.curl_easy_perform(curl);
if (result != c.CURLE_OK) {
std.debug.print("SMTP error: {s}\n", .{c.curl_easy_strerror(result)});
if (result == c.CURLE_PEER_FAILED_VERIFICATION) {
std.debug.print(
"The machine's CA store could not validate the server certificate.\n",
.{});
}
return 1;
}
std.debug.print("Email sent to {s}\n", .{to});
return 0;
}