simple_mailing_list 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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