module SimpleMailingList::System
Constants
- DEFAULT_CONFIGFILE
Private Instance Methods
_add_user(address, user_options = {})
click to toggle source
# File lib/simple_mailing_list/main.rb, line 283 def _add_user(address, user_options = {}) user = User.new user.mail_address = address user.options = JSON.generate(user_options) user.save! @log.info "Add user[#{address}]." end
_check_mail_file(filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 25 def _check_mail_file(filename) mail = Mail.read(filename) begin return if register_mail(mail, filename) return if delete_mail(mail, filename) return if confirm_mail(mail, filename) return if bounced_mail(mail, filename) return if forward_mail(mail, filename) return if unmatched_mail(mail, filename) rescue => e move_mail_file(filename, "error") @log.error "Error Mail[#{File.basename(filename)}]!\n #{error_message(e)}" end end
_check_mails(thread_join = true)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 16 def _check_mails(thread_join = true) threads = receive_mails.map do |mail_filename| Thread.start(File.join(@maillogs_dir, "temp", mail_filename)) do |filename| _check_mail_file(filename) end end threads.each { |thread| thread.join } if thread_join end
_cleanup(delete_maillogs = false)
click to toggle source
# File lib/simple_mailing_list/setup.rb, line 47 def _cleanup(delete_maillogs = false) ActiveRecord::Base.transaction do |a| ActiveRecord::Migration.drop_table(:users) ActiveRecord::Migration.drop_table(:confirmations) FileUtils.remove_entry_secure(@maillogs_dir, true) if delete_maillogs end @log.info "Cleanup successed." end
_delete_old_confirmations()
click to toggle source
# File lib/simple_mailing_list/delete_old.rb, line 26 def _delete_old_confirmations() @log.debug "Delete old confirmations." time = Time.now - @validity_time confirmations = Confirmation.where("created_at < ?", time) num = confirmations.size confirmations.destroy_all() @log.info "#{num} confirmation#{num > 1 ? 's' : ''} #{num > 1 ? 'were' : 'was'} deleted." if num > 0 end
_delete_old_maillogs()
click to toggle source
# File lib/simple_mailing_list/delete_old.rb, line 35 def _delete_old_maillogs() return unless @maillogs_period >= 0 @log.debug "Delete old maillogs." time = Time.now - @maillogs_period num = 0 maillogs = Dir.glob(File.join(@maillogs_dir, "*", "*.eml")).select do |maillog| File.mtime(maillog) < time end num = maillogs.size maillogs.each { |maillog| File.delete(maillog) } @log.info "#{num} maillog#{num > 1 ? 's' : ''} #{num > 1 ? 'were' : 'was'} deleted." if num > 0 end
_delete_user(address)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 291 def _delete_user(address) User.where(mail_address: address).destroy_all() @log.info "Delete user[#{address}]." end
_disable_failed_users(failed_count = 10, time = 5 * 24 * 60 * 60, reset = true)
click to toggle source
# File lib/simple_mailing_list/delete_old.rb, line 8 def _disable_failed_users(failed_count = 10, time = 5 * 24 * 60 * 60, reset = true) @log.debug "Delete failed users." last_failed_at = Time.now - time users = User.where("last_failed_at > ? AND failed_count > ?", last_failed_at, failed_count) users.each do |user| next if user.enabled == 0 @log.info "disable user[#{user.mail_address}]" user.enabled = 0 user.save end if reset User.where(enabled: 1).each do |user| user.failed_count = 0 user.save end end end
_setup()
click to toggle source
# File lib/simple_mailing_list/setup.rb, line 9 def _setup() ActiveRecord::Base.transaction do |a| ActiveRecord::Migration.create_table :users do |t| t.text :mail_address , null: false t.integer :enabled , null: false, default: 1 t.integer :failed_count , null: false, default: 0 t.timestamp :last_failed_at , null: false, default: Time.at(0) t.text :options , null: false, default: "{}" t.timestamps null: false end ActiveRecord::Migration.create_table :confirmations do |t| t.text :mail_address , null: false t.text :check_code , null: false t.text :mode , null: false t.text :options , null: false, default: "{}" t.timestamps null: false end FileUtils.makedirs( %w[ temp register delete register_check delete_check forward bounced unmatched error ].map do |dir| File.join(@maillogs_dir, dir) end ) end @log.info "Setup successed." end
bounced_mail(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 150 def bounced_mail(mail, filename) unless mail.bounced? return false end matched = mail.final_recipient.to_s.match(/;\s*([^@]+@[^@]+)/) if matched mail_address = matched[1] @log.warn "Bounced mail[#{File.basename(filename)}] from #{mail_address}." User.where(mail_address: mail_address).each do |user| user.failed_count += 1 user.last_failed_at = Time.now user.save end else @log.warn "Bounced mail[#{File.basename(filename)}]." end move_mail_file(filename, "bounced") return true end
confirm_mail(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 107 def confirm_mail(mail, filename) address = Array(mail.from).first.to_s return false if address.empty? subject = mail.subject.to_s body = mail.body ? mail.body.decoded : "" body += mail.text_part.decoded.to_s if mail.text_part Confirmation.where(mail_address: address).each do |confirmation| check_code = confirmation.check_code next unless subject.index(check_code) || body.index(check_code) confirm_options = {} subject_text, body_text = case confirmation.mode when "register" @log.info "New register check mail from #{address}." confirm_options = JSON.parse(confirmation.options) _add_user(address, confirm_options) [@register_success_subject, @register_success_body] when "delete" @log.info "New delete check mail from #{address}." _delete_user(address) [@delete_success_subject, @delete_success_body] else next end create_mail( to: address, subject: subject_text, body: body_text, reply_to: @reply_to_address, options: confirm_options ).deliver! sleep @sleep_time1 move_mail_file(filename, "#{confirmation.mode}_check") confirmation.destroy return true end return false end
create_check_code()
click to toggle source
# File lib/simple_mailing_list/main.rb, line 403 def create_check_code() return SecureRandom.base64(9) end
create_forward_mail(mail, subject)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 332 def create_forward_mail(mail, subject) from = @use_address_camouflage ? Array(mail.from).first.to_s : nil new_mail = create_mail( from: from, subject: subject, reply_to: @use_address_camouflage ? nil : @reply_to_address ) new_mail.in_reply_to = mail.in_reply_to if mail.in_reply_to new_mail.references = mail.references if mail.references if (@enable_html_mail || !mail.html_part) new_mail.content_type = mail.content_type new_mail.content_transfer_encoding = mail.content_transfer_encoding mail_head = new_mail.to_s.sub(/\r\n\r\n.*\z/m, "\r\n\r\n") mail_body = mail.raw_source.gsub(/(\r\n|\n|\r)/, "\r\n").sub(/\A.*?\r\n\r\n/m, "") return Mail.read_from_string(mail_head + mail_body) end body_text = mail.body ? mail.body.decoded.to_s : "" if mail.charset && !mail.charset.match(/utf-?8/i) && !mail.text_part body_text.force_encoding(mail.charset) body_text.encode!("utf-8") end body_text = mail.text_part.decoded.to_s if mail.text_part html_text = mail.html_part && mail.html_part.decoded.to_s new_mail.text_part = Mail::Part.new { body body_text } new_mail.html_part = Mail::Part.new { body html_text } if @enable_html_mail && html_text if mail.has_attachments? mail.attachments.each do |attachment| attachment_filename = attachment.filename.to_s if File.extname(attachment_filename).empty? ext = case attachment.mime_type.to_s.downcase when "text/plain" ".txt" when "application/pdf" ".pdf" when "application/rtf" ".rtf" when "application/msword" ".doc" when "application/vnd.ms-excel" ".xls" when "application/vnd.ms-powerpoint" ".ppt" when "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ".docx" when "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ".xlsx" when "application/vnd.openxmlformats-officedocument.presentationml.presentation" ".pptx" when "application/x-js-taro" ".jtd" when "image/gif" ".gif" when "image/jpeg" ".jpeg" when "image/png" ".png" when "application/x-zip-compressed", "application/zip" ".zip" else "" end attachment_filename += ext end new_mail.attachments[attachment_filename] = attachment.decoded end end return new_mail end
create_mail(from: nil, to: nil, subject: "", body: nil, reply_to: nil, options: {})
click to toggle source
# File lib/simple_mailing_list/main.rb, line 320 def create_mail(from: nil, to: nil, subject: "", body: nil, reply_to: nil, options: {}) mail = Mail.new mail.charset = @deliver_server["charset"] || "utf-8" from = @deliver_server["address"] unless from mail.from = options["from"] = from if from mail.to = options["to" ] = to if to mail.reply_to = reply_to if reply_to mail.subject = render_text(subject, options) mail.body = render_text(body , options) if body return mail end
delete_mail(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 79 def delete_mail(mail, filename) rule = find_rule(@delete, mail) address = Array(mail.from).first.to_s return false if !rule || address.empty? @log.info "New delete mail from #{address}." return true if Confirmation.where(mail_address: address).size >= @max_check_times check_code = create_check_code() Confirmation.new( mail_address: address, check_code: check_code, mode: "delete", options: "{}" ).save! mail = create_mail( to: address, subject: @delete_confirm_subject, body: @delete_confirm_body, options: { "checkcode" => check_code } ) mail.subject += check_code unless mail.subject.index(check_code) mail.deliver! sleep @sleep_time1 move_mail_file(filename, "delete") return true end
error_message(error)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 421 def error_message(error) "#{error.inspect} #{error.backtrace.first}" end
find_rule(rules, mail)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 40 def find_rule(rules, mail) mail_body = mail.body ? mail.body.decoded.to_s : "" mail_body += mail.text_part.decoded.to_s if mail.text_part return rules.find do |rule| (!rule["address"] || Array(mail.to).index(rule["address"])) && (!rule["subject"] || mail.subject.to_s.index(rule["subject"])) && (!rule["body" ] || mail_body.index(rule["body"])) end end
forward_mail(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 171 def forward_mail(mail, filename) rule = find_rule(@forward, mail) address = Array(mail.from).first.to_s return false if !rule || address.empty? forward_options = rule["options"] || {} subject = mail.subject.to_s @log.info "New forward mail from #{address}." if @permitted_users permitted_user = @permitted_users.find do |user| (!user["address"] || user["address"] == address) && (!user["check_code"] || subject.encode("utf-8").index(user["check_code"])) end return forward_mail_failed(mail, filename) unless permitted_user if permitted_user["check_code"] subject = subject.encode("utf-8").sub(permitted_user["check_code"], "") end elsif @registered_user_only return forward_mail_failed(mail, filename) unless User.find_by(mail_address: address) end danger_ext = /\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|jar|hta|scr|cpl|lnk)$/i if mail.has_attachments? attachment = mail.attachments.to_a.find do |attachment| attachment.filename.to_s.match(danger_ext) end if attachment @log.warn("Contains danger attachment![#{attachment}@#{File.basename(filename)}]") return forward_mail_failed(mail, filename) end end sendmail = create_forward_mail(mail, subject) users = User.where(enabled: 1).to_a.select do |user| user_options = JSON.parse(user.options) !forward_options.keys.any?{ |key| forward_options[key] != user_options[key] } end users.map! { |user| user.mail_address } users.unshift(address) users.uniq! @log.info "Sending start." domains = { "???" => [] } users.each do |user| domain = user.match(/@([^@]+)$/) ? $1 : "???" domains[domain] ||= [] domains[domain].push(user) end max = domains.values.map(&:size).max max.times do |i| time = Time.now domains.each_value do |address_array| mail_address = address_array[i] next unless mail_address @log.debug "Send to #{mail_address}" sendmail.to = mail_address begin sendmail.deliver! rescue => e @log.warn "Sending Error!(#{mail_address})\n #{error_message(e)}" User.where(mail_address: mail_address).each do |user| user.failed_count += 1 user.last_failed_at = Time.now user.save end end sleep @sleep_time1 end next if Time.now - time > @sleep_time2 || i+1 == max sleep time - Time.now + @sleep_time2 end @log.info "Forward mails to #{users.size} user#{users.size > 0 ? 's' : ''}." if @forward_success_subject && @forward_success_body options = { "subject" => subject.encode("utf-8"), "count" => users.size } reportmail = create_mail( to: address, subject: @forward_success_subject, body: @forward_success_body, options: options ).deliver end move_mail_file(filename, "forward") return true end
forward_mail_failed(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 260 def forward_mail_failed(mail, filename) return true unless @forward_fail_subject && @forward_fail_body options = { "subject" => mail.subject.to_s.encode("utf-8") } sendmail = create_mail( to: Array(mail.from).first.to_s, subject: @forward_fail_subject, body: @forward_fail_body, options: options ).deliver move_mail_file(filename, "forward") return true end
load_configfile(configfile)
click to toggle source
# File lib/simple_mailing_list/configfile.rb, line 7 def load_configfile(configfile) return if @path # basic config = YAML.load(File.read(configfile, encoding: "utf-8")) @path = Dir.getwd # log config["log"] ||= {} file = config["log"]["filename"] ? File.expand_path(Time.now.strftime(config["log"]["filename"]), @path) : STDOUT @log = (config["log"]["rotation"].is_a? Integer) ? Logger.new(file, config["log"]["rotation"], config["log"]["shift_size"] || 1048576) : Logger.new(file, config["log"]["rotation"] || 0) if config["log"]["level"] @log.level = config["log"]["level"].is_a?(String) ? %w[debug info warn error fatal].index(config["log"]["level"].downcase) : config["log"]["level"] end # others @lockfile = config["lockfile"] @maillogs_dir = File.expand_path(config["maillogs_dir"] || "maillogs", @path) @maillogs_period = config["maillogs_period"] || -1 @validity_time = config["validity_time"] || 86400 @max_check_times = config["max_check_times"] || 5 @sleep_time1 = config["sleep_time1"] || 0.1 @sleep_time2 = config["sleep_time2"] || 1.5 @permitted_users = config["permitted_users"] @registered_user_only = !!config["registered_user_only"] @use_address_camouflage = !!config["use_address_camouflage"] @enable_html_mail = !!config["enable_html_mail"] @receive_servers = config["receive_servers"] || [] @deliver_server = config["deliver_server"] || { "protocol" => "sendmail", "charset" => "utf-8", "options" => {} } @register = config["register"] || [] @register_confirm_subject = config["register_confirm_subject"] || "" @register_confirm_body = config["register_confirm_body"] || "" @register_success_subject = config["register_success_subject"] || "" @register_success_body = config["register_success_body"] || "" @delete = config["delete"] || [] @delete_confirm_subject = config["delete_confirm_subject"] || "" @delete_confirm_body = config["delete_confirm_body"] || "" @delete_success_subject = config["delete_success_subject"] || "" @delete_success_body = config["delete_success_body"] || "" @forward = config["forward"] || [] @forward_fail_subject = config["forward_fail_subject"] @forward_fail_body = config["forward_fail_body"] @forward_success_subject = config["forward_success_subject"] @forward_success_body = config["forward_success_body"] @reply_to_address = config["reply_to_address"] # database if config["database_require"] require config["database_require"] else case config["database"]["adapter"] when "mysql2" require "mysql2" when "postgresql" require "pg" when "sqlite3" require "sqlite3" else require config["database"]["adapter"] end end ActiveRecord::Base.establish_connection(config["database"]) # mail server @receive_servers = Array(config["receive_server"]) if config["receive_server"] deliver_server = @deliver_server Mail.defaults do delivery_method(deliver_server["protocol"].to_sym, deliver_server["options"].symbolize_keys) end end
lock() { || ... }
click to toggle source
# File lib/simple_mailing_list/lock.rb, line 5 def lock() if @lockfile open(File.expand_path(@lockfile, @path), "w") do |lockfile| if lockfile.flock(File::LOCK_EX | File::LOCK_NB) @log.debug("Lock successed.") yield lockfile.flock(File::LOCK_UN) @log.debug("Unlocked.") else @log.debug("Lock failed.") end end else yield end end
mail_filename(mail)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 407 def mail_filename(mail) Time.now.strftime("%Y%m%d-%H%M") + "-#{Digest::SHA512.hexdigest(mail.to_s)[0,16]}.eml" end
move_mail_file(filename, dir)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 416 def move_mail_file(filename, dir) new_filename = File.join(@maillogs_dir, dir, File.basename(filename)) File.rename(filename, new_filename) end
receive_mails()
click to toggle source
# File lib/simple_mailing_list/main.rb, line 296 def receive_mails() mail_files = [] lock do @receive_servers.each do |server| begin name = "#{server['options']['user_name']}@#{server['options']['address']}" @log.debug "Check mails to #{name}." Mail.defaults do retriever_method(server["protocol"].to_sym, server["options"].symbolize_keys) end Mail.all(delete_after_find: true).each do |mail| filename = mail_filename(mail) File.binwrite(File.join(@maillogs_dir, "temp", filename), mail.raw_source) @log.info "I got new mail[#{filename}]." mail_files.push(filename) end rescue => e @log.error "Mail Check Error!(#{name})\n #{error_message(e)}" end end end return mail_files end
register_mail(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 50 def register_mail(mail, filename) rule = find_rule(@register, mail) address = Array(mail.from).first.to_s return false if !rule || address.empty? regisiter_options = rule["options"] || {} @log.info "New register mail from #{address}." return true if Confirmation.where(mail_address: address).size >= @max_check_times check_code = create_check_code() Confirmation.new( mail_address: address, check_code: check_code, mode: "register", options: JSON.generate(regisiter_options) ).save! mail = create_mail( to: address, subject: @register_confirm_subject, body: @register_confirm_body, options: regisiter_options.merge({ "checkcode" => check_code }) ) mail.subject += check_code unless mail.subject.index(check_code) mail.deliver! sleep @sleep_time1 move_mail_file(filename, "register") return true end
render_text(text, render_options)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 412 def render_text(text, render_options) render_options.empty? ? text : Liquid::Template.parse(text).render(render_options) end
unmatched_mail(mail, filename)
click to toggle source
# File lib/simple_mailing_list/main.rb, line 276 def unmatched_mail(mail, filename) address = Array(mail.from).first.to_s @log.warn "Unmatched mail[#{File.basename(filename)}] from #{address}." move_mail_file(filename, "unmatched") return true end