tuktuk 0.6.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +19 -0
- data/README.md +43 -0
- data/lib/tuktuk/tuktuk.rb +28 -11
- data/lib/tuktuk/version.rb +2 -2
- data/spec/deliver_spec.rb +139 -13
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 94130469fa773ef26742e1811c40e3dcf6898acf537439470c72c1341cf555c8
|
4
|
+
data.tar.gz: 215936b8fe1b176a642f1c77f04f282f6df1f31e66381b62db09963c81a16044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f8907098711e0e7447165913bf300f956cf72b192f9458f7d6e4b7a2560f2aef12bbe784c9b27cd017d294cb4d4e005a61e595f816148012ee48ce015e51639
|
7
|
+
data.tar.gz: 9f08addb37fd31f1dd1226377746053e424c3386f6a51fcd1e2eff28beb4e786fb603a259877112bd3e23474395780a1a5ec394dc27650058183ac97bce7369d
|
@@ -0,0 +1,19 @@
|
|
1
|
+
name: Ruby
|
2
|
+
on: [push]
|
3
|
+
|
4
|
+
jobs:
|
5
|
+
build:
|
6
|
+
runs-on: ubuntu-latest
|
7
|
+
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v1
|
10
|
+
- name: Set up Ruby 2.6
|
11
|
+
uses: actions/setup-ruby@v1
|
12
|
+
with:
|
13
|
+
ruby-version: 2.6.x
|
14
|
+
- name: Build and test with Rake
|
15
|
+
run: |
|
16
|
+
gem install bundler
|
17
|
+
gem install rspec
|
18
|
+
bundle install --jobs 4 --retry 3
|
19
|
+
rspec
|
data/README.md
CHANGED
@@ -193,6 +193,49 @@ config.action_mailer.tuktuk_settings = {
|
|
193
193
|
}
|
194
194
|
```
|
195
195
|
|
196
|
+
# Example SPK/DKIM/DMARC settings
|
197
|
+
|
198
|
+
If you're sending email from yoursite.com, the SPF record should be set for the APEX/root host, and look like this:
|
199
|
+
|
200
|
+
v=spf1 ip4:[ipv4_address] ip6:[ipv6_address] mx a include:[other_host] ~all
|
201
|
+
|
202
|
+
For example:
|
203
|
+
|
204
|
+
v=spf1 ip4:12.34.56.78 ip6:2600:3c05::f07c:92ff:fe48:b2fd mx a include:mailgun.org ~all
|
205
|
+
|
206
|
+
This tells the receiving server to accept email sent from a) the addresses explicitly mentioned (`ip4` and `ip6`),
|
207
|
+
b) from the hosts mentioned in the `include` statements, as well as c) the hosts listed as `MX` and `A` records for that domain.
|
208
|
+
|
209
|
+
As for DKIM, you should add two TXT records. The first is a simple, short one that goes under the `_domainkey` host,
|
210
|
+
and should contain the following:
|
211
|
+
|
212
|
+
t=y;o=~;
|
213
|
+
|
214
|
+
Then, a second DKIM record should be placed under `[selector]._domainkey` (e.g. `mailer._domainkey`), and should look like this:
|
215
|
+
|
216
|
+
k=rsa; p=MIIBIBANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA[...]DAQAB (public key)
|
217
|
+
|
218
|
+
And finally, your DMARC record goes under the `_dmarc` host, and goes like this:
|
219
|
+
|
220
|
+
v=DMARC1; p=none; rua=mailto:postmaster@yoursite.com; ruf=mailto:postmaster@yoursite.com
|
221
|
+
|
222
|
+
So, in summary:
|
223
|
+
|
224
|
+
(SPF) @.yoursite.com --> v=spf1 ip4:[ipv4_address] ip6:[ipv6_address] mx a include:[other_host] ~all
|
225
|
+
(DKIM1) _domainkey.yoursite.com --> t=y;o=~;
|
226
|
+
(DKIM2) [selector]._domainkey.yoursite.com --> k=rsa; p=MIIBIBANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA[...]DAQAB
|
227
|
+
(DMARC) _dmarc.yoursite.com --> v=DMARC1; p=none; rua=mailto:postmaster@yoursite.com; ruf=mailto:postmaster@yoursite.com
|
228
|
+
|
229
|
+
Now, to check wether your records are OK, you can use the `dig` command like follows:
|
230
|
+
|
231
|
+
dig yoursite.com TXT +short # should output the SPF record, under the root domain
|
232
|
+
dig mailer._domainkey.yoursite.com TXT +short # should output the DKIM record containing the key
|
233
|
+
dig _domainkey.yoursite.com TXT +short # should output the other (short) DKIM
|
234
|
+
dig _dmarc.yoursite.com TXT +short # should output the DMARC record
|
235
|
+
|
236
|
+
Remember you can query your DNS server directly with the `dig` command by adding `@name.server.com`
|
237
|
+
after the `dig` command (e.g. `dig @ns1.linode.com yoursite.com TXT`).
|
238
|
+
|
196
239
|
# Contributions
|
197
240
|
|
198
241
|
You're more than welcome. Send a pull request, including tests, and make sure you don't break anything. That's it.
|
data/lib/tuktuk/tuktuk.rb
CHANGED
@@ -20,6 +20,16 @@ DEFAULTS = {
|
|
20
20
|
:log_to => nil # $stdout,
|
21
21
|
}
|
22
22
|
|
23
|
+
# overwrite Net::SMTP#quit since the connection might have been closed
|
24
|
+
# before we got a chance to say goodbye. swallow the error in that case.
|
25
|
+
class Net::SMTP
|
26
|
+
def quit
|
27
|
+
getok('QUIT')
|
28
|
+
rescue EOFError => e
|
29
|
+
# nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
23
33
|
module Tuktuk
|
24
34
|
|
25
35
|
class << self
|
@@ -30,16 +40,19 @@ module Tuktuk
|
|
30
40
|
|
31
41
|
def deliver(message, opts = {})
|
32
42
|
# raise 'Please pass a valid message object.' unless message[:to]
|
43
|
+
bcc = opts.delete(:bcc) || []
|
44
|
+
bcc = [bcc] if bcc.is_a?(String)
|
45
|
+
|
33
46
|
self.options = opts if opts.any?
|
34
47
|
mail = Package.build(message)
|
35
|
-
response = lookup_and_deliver(mail)
|
48
|
+
response = lookup_and_deliver(mail, bcc)
|
36
49
|
return response, mail
|
37
50
|
end
|
38
51
|
|
39
52
|
# same as deliver but raises error. used by ActionMailer
|
40
|
-
def deliver!(mail)
|
53
|
+
def deliver!(mail, opts = {})
|
41
54
|
@logger = Rails.logger if defined?(Rails) and !config[:log_to]
|
42
|
-
resp, email = deliver(mail)
|
55
|
+
resp, email = deliver(mail, opts)
|
43
56
|
if resp.is_a?(Exception)
|
44
57
|
raise resp
|
45
58
|
else
|
@@ -89,7 +102,9 @@ module Tuktuk
|
|
89
102
|
hash = {}
|
90
103
|
array.each_with_index do |message, i|
|
91
104
|
mail = Package.build(message, i)
|
92
|
-
|
105
|
+
if mail.destinations.count != 1
|
106
|
+
raise ArgumentError, "Invalid destination count: #{mail.destinations.count}"
|
107
|
+
end
|
93
108
|
|
94
109
|
if to = mail.destinations.first and domain = get_domain(to)
|
95
110
|
domain = domain.downcase
|
@@ -109,7 +124,7 @@ module Tuktuk
|
|
109
124
|
servers.any? && servers
|
110
125
|
end
|
111
126
|
|
112
|
-
def lookup_and_deliver(mail,
|
127
|
+
def lookup_and_deliver(mail, bcc = [])
|
113
128
|
if mail.destinations.empty?
|
114
129
|
raise "No destinations found! You need to pass a :to field."
|
115
130
|
end
|
@@ -127,9 +142,10 @@ module Tuktuk
|
|
127
142
|
last_error = nil
|
128
143
|
servers.each do |server|
|
129
144
|
begin
|
130
|
-
response = send_now(mail, server, to)
|
145
|
+
response = send_now(mail, server, to, bcc)
|
131
146
|
break
|
132
147
|
rescue Exception => e # explicitly rescue Exception so we catch Timeout:Error's too
|
148
|
+
logger.error "Error: #{e}"
|
133
149
|
last_error = e
|
134
150
|
end
|
135
151
|
end
|
@@ -210,14 +226,15 @@ module Tuktuk
|
|
210
226
|
responses
|
211
227
|
end
|
212
228
|
|
213
|
-
def send_now(mail, server, to)
|
229
|
+
def send_now(mail, server, to, bcc = [])
|
214
230
|
logger.info "#{to} - Delivering email at #{server}..."
|
231
|
+
logger.info "Including these destinations: #{bcc.inspect}" if bcc && bcc.any?
|
215
232
|
from = get_from(mail)
|
216
233
|
|
217
234
|
response = nil
|
218
235
|
socket = init_connection(server)
|
219
236
|
socket.start(get_helo_domain(from), nil, nil, nil) do |smtp|
|
220
|
-
response = smtp.send_message(get_raw_mail(mail), from, to)
|
237
|
+
response = smtp.send_message(get_raw_mail(mail), from, to, *bcc)
|
221
238
|
logger.info "#{to} - [SENT] #{response.message.strip}"
|
222
239
|
end
|
223
240
|
|
@@ -234,7 +251,7 @@ module Tuktuk
|
|
234
251
|
begin
|
235
252
|
resp = smtp.send_message(get_raw_mail(mail), get_from(mail), mail.to)
|
236
253
|
smtp.send(:getok, 'RSET') if server['hotmail'] # fix for '503 Sender already specified'
|
237
|
-
rescue Net::SMTPFatalError => e # error code 5xx, except for 500, like: 550 Mailbox not found
|
254
|
+
rescue Net::SMTPFatalError, Net::SMTPServerBusy => e # error code 5xx, except for 500, like: 550 Mailbox not found
|
238
255
|
resp = Bounce.type(e)
|
239
256
|
end
|
240
257
|
responses[mail] = resp
|
@@ -243,8 +260,8 @@ module Tuktuk
|
|
243
260
|
end
|
244
261
|
|
245
262
|
responses
|
246
|
-
rescue => e # SMTPServerBusy, SMTPSyntaxError, SMTPUnsupportedCommand, SMTPUnknownError (unexpected reply code)
|
247
|
-
logger.error "[SERVER ERROR: #{server}] #{e.message}"
|
263
|
+
rescue Exception => e # SMTPServerBusy, SMTPSyntaxError, SMTPUnsupportedCommand, SMTPUnknownError (unexpected reply code)
|
264
|
+
logger.error "[SERVER ERROR: #{server}] #{e.class} -> #{e.message}"
|
248
265
|
@last_error = e
|
249
266
|
responses
|
250
267
|
end
|
data/lib/tuktuk/version.rb
CHANGED
data/spec/deliver_spec.rb
CHANGED
@@ -1,8 +1,142 @@
|
|
1
1
|
require './lib/tuktuk/tuktuk'
|
2
2
|
require 'rspec/mocks'
|
3
3
|
|
4
|
+
def email(attrs = {})
|
5
|
+
{ :to => "user#{rand(1000)}@domain.com",
|
6
|
+
:from => 'me@company.com',
|
7
|
+
:subject => 'Test email',
|
8
|
+
:body => 'Hello world.'
|
9
|
+
}.merge(attrs)
|
10
|
+
end
|
11
|
+
|
4
12
|
describe 'deliver' do
|
5
13
|
|
14
|
+
before(:each) do
|
15
|
+
@mock_smtp = double('Net::SMTP', enable_starttls_auto: true, :read_timeout= => true, :open_timeout= => true)
|
16
|
+
@mock_conn = double('SMTP Connection')
|
17
|
+
@mock_smtp.stub(:start).and_yield(@mock_conn)
|
18
|
+
@mock_resp = double('SMTP::Response', message: '250 OK')
|
19
|
+
|
20
|
+
Net::SMTP.stub(:new).and_return(@mock_smtp)
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'single recipient' do
|
24
|
+
|
25
|
+
describe 'when destination is valid (has MX servers)' do
|
26
|
+
|
27
|
+
before do
|
28
|
+
@servers = ['mx1.domain.com', 'mx2.domain.com', 'mx3.domain.com']
|
29
|
+
Tuktuk.stub(:smtp_servers_for_domain).and_return(@servers)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sends message' do
|
33
|
+
msg = email
|
34
|
+
expect(@mock_conn).to receive(:send_message).with(String, msg[:from], msg[:to]).and_return(@mock_resp)
|
35
|
+
Tuktuk.deliver(msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'and bcc is given' do
|
39
|
+
|
40
|
+
let(:bcc_email) { 'bcc@test.com' }
|
41
|
+
|
42
|
+
it 'includes it in destination list' do
|
43
|
+
msg = email
|
44
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to], bcc_email).and_return(@mock_resp)
|
45
|
+
Tuktuk.deliver(msg, bcc: [bcc_email])
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'also works if not passed as array' do
|
49
|
+
msg = email
|
50
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to], bcc_email).and_return(@mock_resp)
|
51
|
+
Tuktuk.deliver(msg, bcc: bcc_email)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'multiple recipients (string list)' do
|
61
|
+
|
62
|
+
describe 'when destination is valid (has MX servers)' do
|
63
|
+
|
64
|
+
before do
|
65
|
+
@servers = ['mx1.domain.com', 'mx2.domain.com', 'mx3.domain.com']
|
66
|
+
Tuktuk.stub(:smtp_servers_for_domain).and_return(@servers)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'sends message' do
|
70
|
+
msg = email(to: 'some@one.com, another@one.com')
|
71
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].split(', ').first).and_return(@mock_resp)
|
72
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].split(', ').last).and_return(@mock_resp)
|
73
|
+
Tuktuk.deliver(msg)
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'and bcc is given' do
|
77
|
+
|
78
|
+
let(:bcc_email) { 'bcc@test.com' }
|
79
|
+
|
80
|
+
it 'includes it in destination list' do
|
81
|
+
msg = email(to: 'some@one.com, another@one.com')
|
82
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].split(', ').first, bcc_email).and_return(@mock_resp)
|
83
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].split(', ').last, bcc_email).and_return(@mock_resp)
|
84
|
+
Tuktuk.deliver(msg, bcc: [bcc_email])
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'also works if not passed as array' do
|
88
|
+
msg = email(to: 'some@one.com, another@one.com')
|
89
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].split(', ').first, bcc_email).and_return(@mock_resp)
|
90
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].split(', ').last, bcc_email).and_return(@mock_resp)
|
91
|
+
Tuktuk.deliver(msg, bcc: bcc_email)
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'multiple recipients (array)' do
|
101
|
+
|
102
|
+
describe 'when destination is valid (has MX servers)' do
|
103
|
+
|
104
|
+
before do
|
105
|
+
@servers = ['mx1.domain.com', 'mx2.domain.com', 'mx3.domain.com']
|
106
|
+
Tuktuk.stub(:smtp_servers_for_domain).and_return(@servers)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'sends message' do
|
110
|
+
msg = email(to: ['some@one.com', 'another@one.com'])
|
111
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].first).and_return(@mock_resp)
|
112
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].last).and_return(@mock_resp)
|
113
|
+
Tuktuk.deliver(msg)
|
114
|
+
end
|
115
|
+
|
116
|
+
describe 'and bcc is given' do
|
117
|
+
|
118
|
+
let(:bcc_email) { 'bcc@test.com' }
|
119
|
+
|
120
|
+
it 'includes it in destination list' do
|
121
|
+
msg = email(to: ['some@one.com', 'another@one.com'])
|
122
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].first, bcc_email).and_return(@mock_resp)
|
123
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].last, bcc_email).and_return(@mock_resp)
|
124
|
+
Tuktuk.deliver(msg, bcc: [bcc_email])
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'also works if not passed as array' do
|
128
|
+
msg = email(to: ['some@one.com', 'another@one.com'])
|
129
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].first, bcc_email).and_return(@mock_resp)
|
130
|
+
expect(@mock_conn).to receive(:send_message).with(instance_of(String), msg[:from], msg[:to].last, bcc_email).and_return(@mock_resp)
|
131
|
+
Tuktuk.deliver(msg, bcc: bcc_email)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
6
140
|
end
|
7
141
|
|
8
142
|
describe 'deliver many' do
|
@@ -17,7 +151,7 @@ describe 'deliver many' do
|
|
17
151
|
it 'raises' do
|
18
152
|
lambda do
|
19
153
|
Tuktuk.deliver_many []
|
20
|
-
end.should raise_error
|
154
|
+
end.should raise_error(ArgumentError)
|
21
155
|
end
|
22
156
|
|
23
157
|
end
|
@@ -26,8 +160,8 @@ describe 'deliver many' do
|
|
26
160
|
|
27
161
|
it 'raises' do
|
28
162
|
lambda do
|
29
|
-
Tuktuk.deliver_many [ email, email(:to => 'one@user.com, two@user.com')]
|
30
|
-
end.should raise_error
|
163
|
+
Tuktuk.deliver_many [ email, email(:to => 'one@user.com, two@user.com') ]
|
164
|
+
end.should raise_error(ArgumentError)
|
31
165
|
end
|
32
166
|
|
33
167
|
end
|
@@ -89,7 +223,7 @@ describe 'deliver many' do
|
|
89
223
|
end
|
90
224
|
|
91
225
|
it 'starts by delivering to first one' do
|
92
|
-
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', [1]).and_return([[1,'ok']])
|
226
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', [1]).and_return([[1, 'ok']])
|
93
227
|
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', [1])
|
94
228
|
end
|
95
229
|
|
@@ -178,7 +312,7 @@ describe 'deliver many' do
|
|
178
312
|
describe 'and other servers are down' do
|
179
313
|
|
180
314
|
before do
|
181
|
-
# TODO: for some reason the :init_connection on line
|
315
|
+
# TODO: for some reason the :init_connection on line 138 is affecting this
|
182
316
|
# this test should pass when running on its own
|
183
317
|
# Tuktuk.should_receive(:init_connection).once.with('mx1.domain.com').and_return(@mock_smtp)
|
184
318
|
# Tuktuk.should_receive(:init_connection).once.with('mx2.domain.com').and_raise('Unable to connect.')
|
@@ -204,12 +338,4 @@ describe 'deliver many' do
|
|
204
338
|
|
205
339
|
end
|
206
340
|
|
207
|
-
def email(attrs = {})
|
208
|
-
{ :to => "user#{rand(1000)}@domain.com",
|
209
|
-
:from => 'me@company.com',
|
210
|
-
:subject => 'Test email',
|
211
|
-
:body => 'Hello world.'
|
212
|
-
}.merge(attrs)
|
213
|
-
end
|
214
|
-
|
215
341
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tuktuk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomás Pollak
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -88,6 +88,7 @@ executables:
|
|
88
88
|
extensions: []
|
89
89
|
extra_rdoc_files: []
|
90
90
|
files:
|
91
|
+
- ".github/workflows/ruby.yml"
|
91
92
|
- ".gitignore"
|
92
93
|
- Gemfile
|
93
94
|
- README.md
|
@@ -123,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
124
|
version: 1.3.6
|
124
125
|
requirements: []
|
125
126
|
rubyforge_project: tuktuk
|
126
|
-
rubygems_version: 2.
|
127
|
+
rubygems_version: 2.7.3
|
127
128
|
signing_key:
|
128
129
|
specification_version: 4
|
129
130
|
summary: SMTP client for Ruby with DKIM support.
|