trikle-mail 0.0.3 → 0.0.9
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/bin/trikle-mail +32 -118
- data/lib/triklemailer.rb +109 -0
- metadata +62 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5d9a65541914cfad901296c5d39cb0fb173ecc85390886f8902f8006d39d37bc
|
4
|
+
data.tar.gz: 0e4ebb9c03c7e2f2a34ab263f6aaa69584cf25095f39e65d89c0a795be503035
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a696c2c3cfb540c202ba997a2f6f553a5031f1d389a3665da7af0558106fc1bf845b9383c7389949097d9a2dbd891229cefd57799da25ceb0c053c33832bb0cb
|
7
|
+
data.tar.gz: 1dd588e80b309f9673e1e9c0f968d6b53e0eb30fc38aa10d27516bf5ec4e2977eca3dd506c0378e81ed48724e003d0ffa6928168bfc0c3dd220ad01353f4ebc2
|
data/bin/trikle-mail
CHANGED
@@ -3,146 +3,60 @@ require 'commander'
|
|
3
3
|
require 'mandrill'
|
4
4
|
require 'csv'
|
5
5
|
require 'mail'
|
6
|
+
require 'triklemailer'
|
6
7
|
|
7
8
|
class TrikleMail
|
8
9
|
extend Commander::Methods
|
9
10
|
|
10
11
|
program :name, 'Trikle Mail'
|
11
|
-
program :version,
|
12
|
-
program :description, 'Send your bulk email in a trikle
|
12
|
+
program :version, Triklemailer::VERSION
|
13
|
+
program :description, 'Send your bulk email from the cli in a trikle'
|
13
14
|
|
14
|
-
default_command :
|
15
|
+
default_command :smtp
|
15
16
|
|
16
17
|
command :smtp do |c|
|
17
|
-
c.syntax = 'trikle-mail
|
18
|
+
c.syntax = 'trikle-mail csv_of_emails.csv [options]'
|
18
19
|
c.description = 'Send a message to each email in the csv using the template'
|
19
20
|
c.option '--host STRING', String, 'The host of the mail server e.g. <mail.gmail.com>'
|
20
21
|
c.option '--port INTEGER', Integer, 'Port number of the mail server e.g. 587'
|
22
|
+
c.option '--domain STRING', String, 'Domain that emails are comming from'
|
21
23
|
c.option '--username STRING', String, 'Username to authenticate with mail server'
|
22
24
|
c.option '--password STRING', String, 'Password to authenticate with mail server'
|
23
25
|
c.option '--from STRING', String, 'The email address to send the email from'
|
26
|
+
c.option '--bcc STRING', String, 'The email address to bcc each message to'
|
27
|
+
c.option '--subject STRING', String, 'The subject line of the message'
|
24
28
|
c.option '--template STRING', String, 'The template to use to send the message'
|
29
|
+
c.option '--attachment STTRING', String, 'The relative file path to your attachement'
|
25
30
|
c.option '--html', 'toggle using html to format email, defaults to no'
|
26
|
-
c.option '--subject STRING', String, 'The subject line of the message'
|
27
31
|
c.option '--hours INTEGER', Integer, 'The number of hours over which to send the emails'
|
28
32
|
c.option '--minutes INTEGER', Integer, 'The number of minutes over which to send the emails'
|
29
33
|
|
30
34
|
c.action do |args, options|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
options.default({hours: 0, minutes: 0, html: false})
|
40
|
-
|
41
|
-
time_range = ((options.hours * 60 * 60) + (options.minutes * 60)) /
|
42
|
-
data_hashes.length.to_f
|
43
|
-
|
44
|
-
data_hashes.each do |hash|
|
45
|
-
|
46
|
-
mail = Mail.new do
|
47
|
-
from hash.fetch(:from, options.from)
|
48
|
-
to hash.fetch(:to, hash.fetch(:email) { raise 'no email address found in to column!' })
|
49
|
-
subject hash.fetch(:subject, options.subject)
|
50
|
-
|
51
|
-
if options.html
|
52
|
-
html_part do
|
53
|
-
content_type 'text/html; charset=UTF-8'
|
54
|
-
body File.read(hash.fetch(:template, options.template)) % hash
|
55
|
-
end
|
56
|
-
else
|
57
|
-
text_part do
|
58
|
-
body File.read(hash.fetch(:template, options.template)) % hash
|
59
|
-
end
|
60
|
-
end
|
35
|
+
recipients = CSV.new(File.new(args[0]), headers: true, header_converters: :symbol).map(&:to_h)
|
36
|
+
sent_file_name = "./#{['sent', options.template.split('.').first].join('_')}.csv"
|
37
|
+
sent =
|
38
|
+
if File.exists?(sent_file_name)
|
39
|
+
CSV.new(File.new(sent_file_name), headers: true, header_converters: :symbol).map { |r| r[:email] }
|
40
|
+
else
|
41
|
+
[]
|
61
42
|
end
|
62
43
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
command :run do |c|
|
81
|
-
c.syntax = 'trikle-mail csv_of_emails.csv [options]'
|
82
|
-
c.description = 'Send a message to each email address in the csv'
|
83
|
-
c.option '--apikey STRING', String, 'The Mandrill API key'
|
84
|
-
c.option '--from_email STRING', String, 'The email address of the sender'
|
85
|
-
c.option '--from_name STRING', String, 'The name of the sender'
|
86
|
-
c.option '--subject STRING', String, 'The subject line of the message'
|
87
|
-
c.option '--template STRING', String, 'The template slug of the message template'
|
88
|
-
c.option '--subaccount STRING', String, 'The mandril subaccount for this batch of email'
|
89
|
-
c.option '--hours INTEGER', Integer, 'The number of hours over which to send the emails'
|
90
|
-
c.option '--minutes INTEGER', Integer, 'The number of minutes over which to send the emails'
|
91
|
-
|
92
|
-
c.action do |args, options|
|
93
|
-
api = Mandrill::API.new(options.apikey)
|
94
|
-
|
95
|
-
title_row, *data_rows = CSV.read(args[0])
|
96
|
-
|
97
|
-
data_hashes = data_rows.map do |row|
|
98
|
-
title_row.map.with_index do |title, index|
|
99
|
-
[title.downcase, row[index]]
|
100
|
-
end.to_h
|
101
|
-
end
|
102
|
-
|
103
|
-
options.default({hours: 0, minutes: 0})
|
104
|
-
time_range = ((options.hours * 60 * 60) + (options.minutes * 60)) /
|
105
|
-
data_hashes.length.to_f
|
106
|
-
|
107
|
-
data_hashes.each do |hash|
|
108
|
-
response = api.messages.send_template(
|
109
|
-
hash['template'] || options.template,
|
110
|
-
[],
|
111
|
-
{
|
112
|
-
subaccount: hash['subaccount'] || options.subaccount,
|
113
|
-
subject: hash['subject'] || options.subject,
|
114
|
-
from_email: hash['from_email'] || options.from_email,
|
115
|
-
from_name: hash['from_name'] || options.from_name,
|
116
|
-
to: [{
|
117
|
-
email: hash['email'],
|
118
|
-
name: hash['name'],
|
119
|
-
type: 'to'
|
120
|
-
}],
|
121
|
-
merge_vars: [{
|
122
|
-
rcpt: hash['email'],
|
123
|
-
vars: hash.map { |key, value| { name: key, content: value } }
|
124
|
-
}]
|
125
|
-
}
|
126
|
-
)
|
127
|
-
TrikleMail.log(response)
|
128
|
-
random_wait_time = rand(0..time_range)
|
129
|
-
puts "waiting #{random_wait_time} seconds"
|
130
|
-
sleep(random_wait_time)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def self.log(json)
|
136
|
-
Array(json).each do |entry|
|
137
|
-
# write to log file
|
138
|
-
File.open('./log.json', 'a') { |f| f.write(JSON.dump(entry) + "\n") }
|
139
|
-
# write nicely to cli
|
140
|
-
puts [
|
141
|
-
entry['status'],
|
142
|
-
'email to',
|
143
|
-
entry['email'],
|
144
|
-
entry['reject_reason'] ? "becuase #{entry['reject_reason']}" : ''
|
145
|
-
].join(' ')
|
44
|
+
mail_options = Triklemailer::Options.new(
|
45
|
+
template: File.read(options.template),
|
46
|
+
template_name: options.template,
|
47
|
+
is_html: options.template.end_with?('html'),
|
48
|
+
from: options.from,
|
49
|
+
bcc: options.bcc,
|
50
|
+
subject: options.subject,
|
51
|
+
port: options.port,
|
52
|
+
host: options.host,
|
53
|
+
username: options.username,
|
54
|
+
password: options.password,
|
55
|
+
domain: options.domain,
|
56
|
+
attachment: options.attachment
|
57
|
+
)
|
58
|
+
|
59
|
+
Triklemailer.send_mail(mail_options, recipients, sent)
|
146
60
|
end
|
147
61
|
end
|
148
62
|
end
|
data/lib/triklemailer.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'mail'
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
module Triklemailer
|
5
|
+
VERSION = '0.0.9'
|
6
|
+
Options = Struct.new(
|
7
|
+
:template, :template_name, :is_html, :from, :subject,
|
8
|
+
:host, :port, :username, :password, :bcc, :domain,
|
9
|
+
:attachment,
|
10
|
+
keyword_init: true
|
11
|
+
)
|
12
|
+
|
13
|
+
# It should take:
|
14
|
+
# * A list of emails and data
|
15
|
+
# * SMTP account details
|
16
|
+
# * A template
|
17
|
+
# * SMTP Server details
|
18
|
+
#
|
19
|
+
# and it should send an email to each email address from the csv using
|
20
|
+
# the template provided, filled in with the meta data provided for the
|
21
|
+
# email address. When each email is sent it should record that in a
|
22
|
+
# separate csv called sent_<template-name>.csv. Failures to send a
|
23
|
+
# messages should not be recorded. When sending emails it should check
|
24
|
+
# the sent_<template-name>.csv and if it exists, only send emails to
|
25
|
+
# email address not included in the sent_<template-name>.csv file.
|
26
|
+
#
|
27
|
+
# Read provided csv
|
28
|
+
# Check for sent_ csv
|
29
|
+
# For each email, meta_data in provided csv
|
30
|
+
# If email in sent_ csv
|
31
|
+
# Next email
|
32
|
+
#
|
33
|
+
# Read template file
|
34
|
+
# Apply meta_data to template file
|
35
|
+
# Send email with data from cli or meta data with template
|
36
|
+
# If there was an error sending
|
37
|
+
# Next email
|
38
|
+
# Record email sent to template in sent_ csv
|
39
|
+
#
|
40
|
+
def self.send_mail(options, recipients, sent)
|
41
|
+
cleaned_template = options.template.gsub(/%(?!{)/, '%%')
|
42
|
+
|
43
|
+
recipients.
|
44
|
+
reject { |recipient| sent.include?(recipient[:email]) }.
|
45
|
+
each do |recipient|
|
46
|
+
mail = Mail.new do
|
47
|
+
from recipient.fetch(:from, options.from)
|
48
|
+
to recipient.fetch(:to, recipient.fetch(:email) {
|
49
|
+
raise "Some rows are missing either a 'to' or 'email' column."
|
50
|
+
})
|
51
|
+
bcc options.bcc if options.bcc
|
52
|
+
subject recipient.fetch(:subject, options.subject)
|
53
|
+
|
54
|
+
if options.is_html
|
55
|
+
html_part do
|
56
|
+
content_type 'text/html; charset=UTF-8'
|
57
|
+
body cleaned_template % recipient
|
58
|
+
end
|
59
|
+
else
|
60
|
+
text_part do
|
61
|
+
body cleaned_template % recipient
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
add_file(File.expand_path(options.attachment)) if options.attachment
|
66
|
+
end
|
67
|
+
|
68
|
+
if block_given?
|
69
|
+
yield(mail, recipient)
|
70
|
+
else
|
71
|
+
mail.delivery_method :smtp, {
|
72
|
+
address: recipient.fetch(:host, options.host),
|
73
|
+
port: recipient.fetch(:port, options.port),
|
74
|
+
user_name: recipient.fetch(:username, options.username),
|
75
|
+
password: recipient.fetch(:password, options.password),
|
76
|
+
authentication: :login,
|
77
|
+
enable_starttls_auto: true
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
begin
|
82
|
+
mail.deliver!
|
83
|
+
# might fail in here though...
|
84
|
+
log_sent(
|
85
|
+
recipient.fetch(:to, recipient[:email]),
|
86
|
+
options.template_name)
|
87
|
+
rescue => e
|
88
|
+
puts "Failed to send email to #{recipient[:email]}."
|
89
|
+
puts e.class
|
90
|
+
puts e
|
91
|
+
puts e.backtrace
|
92
|
+
next
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.log_sent(email, template)
|
98
|
+
log_file_name = "./#{['sent', template.split('.').first].join('_')}.csv"
|
99
|
+
|
100
|
+
CSV.open(
|
101
|
+
log_file_name,
|
102
|
+
'a',
|
103
|
+
write_headers: !File.exists?(log_file_name),
|
104
|
+
headers: ['email', 'sent_at']
|
105
|
+
) do |csv|
|
106
|
+
csv << [email, Time.now]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trikle-mail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jasper Lyons
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
date: 2017-10-18 00:00:00.000000000 Z
|
@@ -38,6 +38,62 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mail
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mail
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
41
97
|
description: Send bulk email in a trickle over mandrill
|
42
98
|
email: jasper.lyons@gmail.com
|
43
99
|
executables:
|
@@ -46,11 +102,12 @@ extensions: []
|
|
46
102
|
extra_rdoc_files: []
|
47
103
|
files:
|
48
104
|
- bin/trikle-mail
|
105
|
+
- lib/triklemailer.rb
|
49
106
|
homepage: ''
|
50
107
|
licenses:
|
51
108
|
- MIT
|
52
109
|
metadata: {}
|
53
|
-
post_install_message:
|
110
|
+
post_install_message:
|
54
111
|
rdoc_options: []
|
55
112
|
require_paths:
|
56
113
|
- lib
|
@@ -65,10 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
122
|
- !ruby/object:Gem::Version
|
66
123
|
version: '0'
|
67
124
|
requirements: []
|
68
|
-
|
69
|
-
|
70
|
-
signing_key:
|
125
|
+
rubygems_version: 3.1.4
|
126
|
+
signing_key:
|
71
127
|
specification_version: 4
|
72
128
|
summary: Send a trickle of mail
|
73
129
|
test_files: []
|
74
|
-
has_rdoc:
|