simple_mailing_list 0.1.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.
@@ -0,0 +1,394 @@
1
+ require "json"
2
+ require "securerandom"
3
+ require "digest/sha2"
4
+
5
+ require "liquid"
6
+
7
+ require "simple_mailing_list/lock"
8
+
9
+ class User < ActiveRecord::Base ; end
10
+ class Confirmation < ActiveRecord::Base ; end
11
+
12
+ module SimpleMailingList
13
+ module System
14
+ private
15
+
16
+ def _check_mails(thread_join = true)
17
+ threads = receive_mails.map do |mail_filename|
18
+ Thread.start(File.join(@maillogs_dir, "temp", mail_filename)) do |filename|
19
+ _check_mail_file(filename)
20
+ end
21
+ end
22
+ threads.each { |thread| thread.join } if thread_join
23
+ end
24
+
25
+ def _check_mail_file(filename)
26
+ mail = Mail.read(filename)
27
+ begin
28
+ return if register_mail(mail, filename)
29
+ return if delete_mail(mail, filename)
30
+ return if confirm_mail(mail, filename)
31
+ return if bounced_mail(mail, filename)
32
+ return if forward_mail(mail, filename)
33
+ return if unmatched_mail(mail, filename)
34
+ rescue => e
35
+ move_mail_file(filename, "error")
36
+ @log.error "Error Mail[#{File.basename(filename)}]!\n #{error_message(e)}"
37
+ end
38
+ end
39
+
40
+ def find_rule(rules, mail)
41
+ mail_body = mail.body ? mail.body.decoded.to_s : ""
42
+ mail_body += mail.text_part.decoded.to_s if mail.text_part
43
+ return rules.find do |rule|
44
+ (!rule["address"] || Array(mail.to).index(rule["address"])) &&
45
+ (!rule["subject"] || mail.subject.to_s.index(rule["subject"])) &&
46
+ (!rule["body" ] || mail_body.index(rule["body"]))
47
+ end
48
+ end
49
+
50
+ def register_mail(mail, filename)
51
+ rule = find_rule(@register, mail)
52
+ address = Array(mail.from).first.to_s
53
+ return false if !rule || address.empty?
54
+ regisiter_options = rule["options"] || {}
55
+ @log.info "New register mail from #{address}."
56
+ return true if Confirmation.where(mail_address: address).size >= @max_check_times
57
+
58
+ check_code = create_check_code()
59
+ Confirmation.new(
60
+ mail_address: address,
61
+ check_code: check_code,
62
+ mode: "register",
63
+ options: JSON.generate(regisiter_options)
64
+ ).save!
65
+
66
+ mail = create_mail(
67
+ to: address,
68
+ subject: @register_confirm_subject,
69
+ body: @register_confirm_body,
70
+ options: regisiter_options.merge({ "checkcode" => check_code })
71
+ )
72
+ mail.subject += check_code unless mail.subject.index(check_code)
73
+ mail.deliver!
74
+ sleep @sleep_time1
75
+ move_mail_file(filename, "register")
76
+ return true
77
+ end
78
+
79
+ def delete_mail(mail, filename)
80
+ rule = find_rule(@delete, mail)
81
+ address = Array(mail.from).first.to_s
82
+ return false if !rule || address.empty?
83
+ @log.info "New delete mail from #{address}."
84
+ return true if Confirmation.where(mail_address: address).size >= @max_check_times
85
+
86
+ check_code = create_check_code()
87
+ Confirmation.new(
88
+ mail_address: address,
89
+ check_code: check_code,
90
+ mode: "delete",
91
+ options: "{}"
92
+ ).save!
93
+
94
+ mail = create_mail(
95
+ to: address,
96
+ subject: @delete_confirm_subject,
97
+ body: @delete_confirm_body,
98
+ options: { "checkcode" => check_code }
99
+ )
100
+ mail.subject += check_code unless mail.subject.index(check_code)
101
+ mail.deliver!
102
+ sleep @sleep_time1
103
+ move_mail_file(filename, "delete")
104
+ return true
105
+ end
106
+
107
+ def confirm_mail(mail, filename)
108
+ address = Array(mail.from).first.to_s
109
+ return false if address.empty?
110
+ subject = mail.subject.to_s
111
+ body = mail.body ? mail.body.decoded : ""
112
+ body += mail.text_part.decoded.to_s if mail.text_part
113
+
114
+ Confirmation.where(mail_address: address).each do |confirmation|
115
+ check_code = confirmation.check_code
116
+ next unless subject.index(check_code) || body.index(check_code)
117
+
118
+ confirm_options = {}
119
+ subject_text, body_text = case confirmation.mode
120
+ when "register"
121
+ @log.info "New register check mail from #{address}."
122
+ confirm_options = JSON.parse(confirmation.options)
123
+ _add_user(address, confirm_options)
124
+ [@register_success_subject, @register_success_body]
125
+ when "delete"
126
+ @log.info "New delete check mail from #{address}."
127
+ _delete_user(address)
128
+ [@delete_success_subject, @delete_success_body]
129
+ else
130
+ next
131
+ end
132
+
133
+ create_mail(
134
+ to: address,
135
+ subject: subject_text,
136
+ body: body_text,
137
+ reply_to: @reply_to_address,
138
+ options: confirm_options
139
+ ).deliver!
140
+ sleep @sleep_time1
141
+
142
+ move_mail_file(filename, "#{confirmation.mode}_check")
143
+ confirmation.destroy
144
+ return true
145
+ end
146
+
147
+ return false
148
+ end
149
+
150
+ def bounced_mail(mail, filename)
151
+ unless mail.bounced?
152
+ return false
153
+ end
154
+
155
+ matched = mail.final_recipient.to_s.match(/;\s*([^@]+@[^@]+)/)
156
+ if matched
157
+ mail_address = matched[1]
158
+ @log.warn "Bounced mail[#{File.basename(filename)}] from #{mail_address}."
159
+ User.where(mail_address: mail_address).each do |user|
160
+ user.failed_count += 1
161
+ user.last_failed_at = Time.now
162
+ user.save
163
+ end
164
+ else
165
+ @log.warn "Bounced mail[#{File.basename(filename)}]."
166
+ end
167
+ move_mail_file(filename, "bounced")
168
+ return true
169
+ end
170
+
171
+ def forward_mail(mail, filename)
172
+ rule = find_rule(@forward, mail)
173
+ address = Array(mail.from).first.to_s
174
+ return false if !rule || address.empty?
175
+ forward_options = rule["options"] || {}
176
+ subject = mail.subject.to_s
177
+ @log.info "New forward mail from #{address}."
178
+ if @permitted_users
179
+ permitted_user = @permitted_users.find do |user|
180
+ (!user["address"] || user["address"] == address) &&
181
+ (!user["check_code"] || subject.index(["check_code"]))
182
+ end
183
+ return true unless permitted_user
184
+ subject.sub!(permitted_user["check_code"], "") if permitted_user["check_code"]
185
+ elsif @registered_user_only
186
+ return true unless User.find_by(mail_address: address)
187
+ end
188
+ danger_ext = /\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|jar|hta|scr|cpl|lnk)$/i
189
+ if mail.has_attachments?
190
+ attachment = mail.attachments.to_a.find do |attachment|
191
+ attachment.filename.to_s.match(danger_ext)
192
+ end
193
+ if attachment
194
+ @log.warn("Contains danger attachment![#{attachment}@#{File.basename(filename)}]")
195
+ return true
196
+ end
197
+ end
198
+
199
+ sendmail = create_forward_mail(mail, subject)
200
+
201
+ users = User.all.to_a.select do |user|
202
+ user_options = JSON.parse(user.options)
203
+ !forward_options.keys.any?{ |key| forward_options[key] != user_options[key] }
204
+ end
205
+ users.map! { |user| user.mail_address }
206
+ users.uniq!
207
+ users.delete(address)
208
+
209
+ domains = { "???" => [] }
210
+ users.each do |user|
211
+ domain = user.match(/@([^@]+)$/) ? $1 : "???"
212
+ domains[domain] ||= []
213
+ domains[domain].push(user)
214
+ end
215
+ max = domains.values.map(&:size).max
216
+ domains["???"][max] = address
217
+ 0.upto(max) do |i|
218
+ time = Time.now
219
+ domains.each_value do |address_array|
220
+ mail_address = address_array[i]
221
+ next unless mail_address
222
+
223
+ @log.debug "Send to #{mail_address}"
224
+ sendmail.to = mail_address
225
+ begin
226
+ sendmail.deliver!
227
+ rescue => e
228
+ @log.warn "Sending Error!(#{mail_address})\n #{error_message(e)}"
229
+ User.where(mail_address: mail_address).each do |user|
230
+ user.failed_count += 1
231
+ user.last_failed_at = Time.now
232
+ user.save
233
+ end
234
+ end
235
+ sleep @sleep_time1
236
+ end
237
+ next if Time.now - time > @sleep_time2 || i+1 == max
238
+ sleep time - Time.now + @sleep_time2
239
+ end
240
+ @log.info "Forward mails to #{users.size + 1} user#{users.size > 0 ? 's' : ''}."
241
+ move_mail_file(filename, "forward")
242
+ return true
243
+ end
244
+
245
+ def unmatched_mail(mail, filename)
246
+ address = Array(mail.from).first.to_s
247
+ @log.warn "Unmatched mail[#{File.basename(filename)}] from #{address}."
248
+ move_mail_file(filename, "unmatched")
249
+ return true
250
+ end
251
+
252
+ def _add_user(address, user_options = {})
253
+ user = User.new
254
+ user.mail_address = address
255
+ user.options = JSON.generate(user_options)
256
+ user.save!
257
+ @log.info "Add user[#{address}]."
258
+ end
259
+
260
+ def _delete_user(address)
261
+ User.where(mail_address: address).destroy_all()
262
+ @log.info "Delete user[#{address}]."
263
+ end
264
+
265
+ def receive_mails()
266
+ mail_files = []
267
+ lock do
268
+ @receive_servers.each do |server|
269
+ begin
270
+ name = "#{server['options']['user_name']}@#{server['options']['address']}"
271
+ @log.debug "Check mails to #{name}."
272
+ Mail.defaults do
273
+ retriever_method(server["protocol"].to_sym, server["options"].symbolize_keys)
274
+ end
275
+ Mail.all(delete_after_find: true).each do |mail|
276
+ filename = mail_filename(mail)
277
+ File.binwrite(File.join(@maillogs_dir, "temp", filename), mail.raw_source)
278
+ @log.info "I got new mail[#{filename}]."
279
+ mail_files.push(filename)
280
+ end
281
+ rescue => e
282
+ @log.error "Mail Check Error!(#{name})\n #{error_message(e)}"
283
+ end
284
+ end
285
+ end
286
+ return mail_files
287
+ end
288
+
289
+ def create_mail(from: nil, to: nil, subject: "", body: nil, reply_to: nil, options: {})
290
+ mail = Mail.new
291
+ mail.charset = @deliver_server["charset"] || "utf-8"
292
+ from = @deliver_server["address"] unless from
293
+ mail.from = options["from"] = from if from
294
+ mail.to = options["to" ] = to if to
295
+ mail.reply_to = reply_to if reply_to
296
+ mail.subject = render_text(subject, options)
297
+ mail.body = render_text(body , options) if body
298
+ return mail
299
+ end
300
+
301
+ def create_forward_mail(mail, subject)
302
+ from = @use_address_camouflage ? Array(mail.from).first.to_s : nil
303
+ new_mail = create_mail(
304
+ from: from,
305
+ subject: subject,
306
+ reply_to: @use_address_camouflage ? nil : @reply_to_address
307
+ )
308
+ new_mail.in_reply_to = mail.in_reply_to if mail.in_reply_to
309
+ new_mail.references = mail.references if mail.references
310
+
311
+ if (@enable_html_mail || !mail.html_part)
312
+ new_mail.content_type = mail.content_type
313
+ new_mail.content_transfer_encoding = mail.content_transfer_encoding
314
+ mail_head = new_mail.to_s.sub(/\r\n\r\n.*\z/m, "\r\n\r\n")
315
+ mail_body = mail.raw_source.gsub(/(\r\n|\n|\r)/, "\r\n").sub(/\A.*?\r\n\r\n/m, "")
316
+ return Mail.read_from_string(mail_head + mail_body)
317
+ end
318
+
319
+ body_text = mail.body ? mail.body.decoded.to_s : ""
320
+ if mail.charset && !mail.charset.match(/utf-?8/i) && !mail.text_part
321
+ body_text.force_encoding(mail.charset)
322
+ body_text.encode!("utf-8")
323
+ end
324
+ body_text = mail.text_part.decoded.to_s if mail.text_part
325
+ html_text = mail.html_part && mail.html_part.decoded.to_s
326
+ new_mail.text_part = Mail::Part.new { body body_text }
327
+ new_mail.html_part = Mail::Part.new { body html_text } if @enable_html_mail && html_text
328
+ if mail.has_attachments?
329
+ mail.attachments.each do |attachment|
330
+ attachment_filename = attachment.filename.to_s
331
+ if File.extname(attachment_filename).empty?
332
+ ext = case attachment.mime_type.to_s.downcase
333
+ when "text/plain"
334
+ ".txt"
335
+ when "application/pdf"
336
+ ".pdf"
337
+ when "application/rtf"
338
+ ".rtf"
339
+ when "application/msword"
340
+ ".doc"
341
+ when "application/vnd.ms-excel"
342
+ ".xls"
343
+ when "application/vnd.ms-powerpoint"
344
+ ".ppt"
345
+ when "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
346
+ ".docx"
347
+ when "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
348
+ ".xlsx"
349
+ when "application/vnd.openxmlformats-officedocument.presentationml.presentation"
350
+ ".pptx"
351
+ when "application/x-js-taro"
352
+ ".jtd"
353
+ when "image/gif"
354
+ ".gif"
355
+ when "image/jpeg"
356
+ ".jpeg"
357
+ when "image/png"
358
+ ".png"
359
+ when "application/x-zip-compressed", "application/zip"
360
+ ".zip"
361
+ else
362
+ ""
363
+ end
364
+ attachment_filename += ext
365
+ end
366
+ new_mail.attachments[attachment_filename] = attachment.decoded
367
+ end
368
+ end
369
+ return new_mail
370
+ end
371
+
372
+ def create_check_code()
373
+ return SecureRandom.base64(9)
374
+ end
375
+
376
+ def mail_filename(mail)
377
+ Time.now.strftime("%Y%m%d-%H%M") +
378
+ "-#{Digest::SHA512.hexdigest(mail.to_s)[0,16]}.eml"
379
+ end
380
+
381
+ def render_text(text, render_options)
382
+ render_options.empty? ? text : Liquid::Template.parse(text).render(render_options)
383
+ end
384
+
385
+ def move_mail_file(filename, dir)
386
+ new_filename = File.join(@maillogs_dir, dir, File.basename(filename))
387
+ File.rename(filename, new_filename)
388
+ end
389
+
390
+ def error_message(error)
391
+ "#{error.inspect} #{error.backtrace.first}"
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,56 @@
1
+ require "fileutils"
2
+
3
+ require "active_record"
4
+
5
+ module SimpleMailingList
6
+ module System
7
+ private
8
+
9
+ def _setup()
10
+ ActiveRecord::Base.transaction do |a|
11
+ ActiveRecord::Migration.create_table :users do |t|
12
+ t.text :mail_address , null: false
13
+ t.integer :failed_count , null: false, default: 0
14
+ t.timestamp :last_failed_at , null: false, default: Time.at(0)
15
+ t.text :options , null: false, default: "{}"
16
+ t.timestamps null: false
17
+ end
18
+
19
+ ActiveRecord::Migration.create_table :confirmations do |t|
20
+ t.text :mail_address , null: false
21
+ t.text :check_code , null: false
22
+ t.text :mode , null: false
23
+ t.text :options , null: false, default: "{}"
24
+ t.timestamps null: false
25
+ end
26
+
27
+ FileUtils.makedirs(
28
+ %w[
29
+ temp
30
+ register
31
+ delete
32
+ register_check
33
+ delete_check
34
+ forward
35
+ bounced
36
+ unmatched
37
+ error
38
+ ].map do |dir|
39
+ File.join(@maillogs_dir, dir)
40
+ end
41
+ )
42
+ end
43
+ @log.info "Setup successed."
44
+ end
45
+
46
+ def _cleanup(delete_maillogs = false)
47
+ ActiveRecord::Base.transaction do |a|
48
+ ActiveRecord::Migration.drop_table(:users)
49
+ ActiveRecord::Migration.drop_table(:confirmations)
50
+
51
+ FileUtils.remove_entry_secure(@maillogs_dir, true) if delete_maillogs
52
+ end
53
+ @log.info "Cleanup successed."
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleMailingList
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,134 @@
1
+ require "yaml"
2
+ require "logger"
3
+
4
+ require "mail"
5
+ require "active_record"
6
+ require "thor"
7
+
8
+ require "simple_mailing_list/configfile"
9
+ require "simple_mailing_list/version"
10
+
11
+ module SimpleMailingList
12
+ class CLI < Thor
13
+ include SimpleMailingList::System
14
+ package_name "SimpleMailingList"
15
+ default_command :main_jobs
16
+ map "--version" => :version
17
+ class_option :configfile,
18
+ aliases: "-c",
19
+ default: DEFAULT_CONFIGFILE,
20
+ desc: "configfile(YAML format) path."
21
+
22
+ def self.exit_on_failure?
23
+ true
24
+ end
25
+
26
+ desc "setup", "create tables and maillog_dir."
27
+ def setup()
28
+ require "simple_mailing_list/setup"
29
+ load_configfile(options[:configfile])
30
+ _setup()
31
+ end
32
+
33
+ desc "cleanup", "drop tables."
34
+ option :delete_maillogs,
35
+ default: false,
36
+ type: :boolean
37
+ def cleanup()
38
+ require "simple_mailing_list/setup"
39
+ load_configfile(options[:configfile])
40
+ _cleanup(options[:delete_maillogs])
41
+ end
42
+
43
+ desc "add_user MAIL_ADDRESS [JSON_FORMAT_OPTION]", "add a user."
44
+ def add_user(address, user_options="{}")
45
+ require "simple_mailing_list/main"
46
+ require "json"
47
+ load_configfile(options[:configfile])
48
+ _add_user(address, JSON.parse(user_options))
49
+ end
50
+
51
+ desc "delete_user MAIL_ADDRESS", "delete a user."
52
+ def delete_user(address)
53
+ require "simple_mailing_list/main"
54
+ load_configfile(options[:configfile])
55
+ _delete_user(address)
56
+ end
57
+
58
+ desc "delete_failed_users", "delete users who do not receive mails."
59
+ option :failed_count,
60
+ aliases: "-f",
61
+ default: 10,
62
+ type: :numeric
63
+ option :failed_time,
64
+ aliases: "-t",
65
+ default: 5 * 24 * 60 * 60,
66
+ type: :numeric
67
+ def delete_failed_users()
68
+ require "simple_mailing_list/delete_old"
69
+ load_configfile(options[:configfile])
70
+ _delete_failed_users(options[:failed_count], options[:failed_time])
71
+ end
72
+
73
+ desc "delete_old_confirmations", "delete old confirmations."
74
+ def delete_old_confirmations()
75
+ require "simple_mailing_list/delete_old"
76
+ load_configfile(options[:configfile])
77
+ _delete_old_confirmations()
78
+ end
79
+
80
+ desc "delete_old_maillogs", "delete old maillogs."
81
+ def delete_old_maillogs()
82
+ require "simple_mailing_list/delete_old"
83
+ load_configfile(options[:configfile])
84
+ _delete_old_maillogs()
85
+ end
86
+
87
+ desc "check_mails", "check new mails."
88
+ def check_mails()
89
+ require "simple_mailing_list/main"
90
+ load_configfile(options[:configfile])
91
+ _check_mails()
92
+ end
93
+
94
+ desc "check_mail_file [MAIL_FILE]", "check new mail from file."
95
+ def check_mail_file(mailfile)
96
+ require "simple_mailing_list/main"
97
+ load_configfile(options[:configfile])
98
+ _check_mail_file(mailfile)
99
+ end
100
+
101
+ desc "main_jobs", "check new mails and delete old confirmations and maillogs."
102
+ def main_jobs()
103
+ require "simple_mailing_list/main"
104
+ require "simple_mailing_list/delete_old"
105
+ load_configfile(options[:configfile])
106
+ _check_mails()
107
+ _delete_old_confirmations()
108
+ _delete_old_maillogs()
109
+ end
110
+
111
+ desc "loop", "loop main_jobs."
112
+ option :sleep_time,
113
+ aliases: "-s",
114
+ default: 10,
115
+ type: :numeric,
116
+ desc: "wait time on a loop."
117
+ def loop()
118
+ require "simple_mailing_list/main"
119
+ require "simple_mailing_list/delete_old"
120
+ load_configfile(options[:configfile])
121
+ loop do
122
+ _check_mails(false)
123
+ _delete_old_confirmations()
124
+ _delete_old_maillogs()
125
+ sleep options[:sleep_time]
126
+ end
127
+ end
128
+
129
+ desc "version", "output version."
130
+ def version()
131
+ puts "Simple Mailing List - #{SimpleMailingList::VERSION}"
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,35 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "simple_mailing_list/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "simple_mailing_list"
8
+ spec.version = SimpleMailingList::VERSION
9
+ spec.authors = ["nodai2hITC"]
10
+ spec.email = ["nodai2h.itc@gmail.com"]
11
+
12
+ spec.summary = %q{Simple Mailing List System.}
13
+ spec.description = %q{This is a simple mailing list system.}
14
+ spec.homepage = "https://github.com/nodai2hITC/simple_mailing_list"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.16"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+
27
+ spec.add_dependency "mail"
28
+ spec.add_dependency "activerecord"
29
+ spec.add_dependency "liquid"
30
+ spec.add_dependency "daemons"
31
+ spec.add_dependency "thor"
32
+ # spec.add_dependency "mysql2"
33
+ # spec.add_dependency "pg"
34
+ spec.add_dependency "sqlite3"
35
+ end