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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9b23aad601ecb20c8e9f2a61ca0c5d8740fe08fb
4
- data.tar.gz: ccd26989dcc9cf8ea0cd124bc117cd8aa4ebe337
2
+ SHA256:
3
+ metadata.gz: 94130469fa773ef26742e1811c40e3dcf6898acf537439470c72c1341cf555c8
4
+ data.tar.gz: 215936b8fe1b176a642f1c77f04f282f6df1f31e66381b62db09963c81a16044
5
5
  SHA512:
6
- metadata.gz: 573938ae9b56c2fea03e8cf19ae133b298e146232813b022ee2cc17e275e68cf98a353e4863eec720da3a898a7e78d3ba59926963db846bc02f215cb87eaf92f
7
- data.tar.gz: 0fb02b910a04a3f0e2472e7e65264add4eed11cc12a46f68b3a1adf820f9ff615392d24a65831da98aafa95f0a909d828f68454192111706aad905ee5fc487a1
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.
@@ -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
- raise "Invalid destination count: #{mail.destinations.count}" if mail.destinations.count != 1
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, attempt = 1)
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
@@ -1,7 +1,7 @@
1
1
  module Tuktuk
2
2
  MAJOR = 0
3
- MINOR = 6
4
- PATCH = 3
3
+ MINOR = 8
4
+ PATCH = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
@@ -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 138 is affecting this
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.6.3
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: 2018-12-03 00:00:00.000000000 Z
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.6.13
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.