# encoding: utf-8 =begin rdoc Author:: Chris Hauboldt (mailto:biz@lnstar.com) Copyright:: 2009 Lone Star Internet Inc. Mailing is used to send a Mailable object to an MailingList. It is 'ready' to send when its status is 'scheduled' and 'shcheduled_at' is in the past. MailingJob will poll for 'ready' Mailings and send them. Statuses pending - initial state - mailing is waiting to be tested and scheduled - will not send scheduled - mailing will be sent when it is 'ready' (its status is 'scheduled' and 'shcheduled_at' is in the past) processing - MailingJob is sending the messages for the mailing completed - Mailing has been sent =end module MailManager class Mailing < ActiveRecord::Base self.table_name = "#{MailManager.table_prefix}mailings" has_many :messages, :class_name => 'MailManager::Message' has_many :test_messages, :class_name => 'MailManager::TestMessage' has_many :bounces, :class_name => 'MailManager::Bounce' has_and_belongs_to_many :mailing_lists, :class_name => 'MailManager::MailingList', :join_table => "#{MailManager.table_prefix}mailing_lists_#{MailManager.table_prefix}mailings" #FIXME why does this break? belongs_to :mailable, :polymorphic => true accepts_nested_attributes_for :mailable attr_accessor :bounce_count validates_presence_of :subject #validates_presence_of :mailable scope :ready, lambda {{:conditions => ["(status='scheduled' AND scheduled_at < ?) OR status='resumed'",Time.now.utc]}} scope :by_statuses, lambda {|*statuses| {:conditions => ["status in (#{statuses.collect{|bindings,status| '?'}.join(",")})",statuses].flatten}} def self.with_bounces(bounce_status=nil) bounce_status_condition = bounce_status.present? ? ActiveRecord::Base.send(:sanitize_sql_array,[" WHERE status=?", bounce_status]) : '' bounce_query = "SELECT mailing_id, COUNT(id) AS count from #{MailManager.table_prefix}bounces #{bounce_status_condition} group by mailing_id" bounce_data = Bounce.connection.execute(bounce_query).inject({}){|hash,(mailing_id,count)| hash.merge(mailing_id => count)} mailings = scoped mailings = mailings.where("id in (#{bounce_data.keys.select(&:present?).join(',')})") if bounce_data.keys.select(&:present?).present? mailings.order("created_at desc").map{|mailing| mailing.bounce_count = bounce_data[mailing.id]; mailing} end include StatusHistory override_statuses(['pending','scheduled','processing','paused','resumed','cancelled','completed'],'pending') before_create :set_default_status attr_protected :id def send_one_off_message(contact) message = Message.new message.contact_id = contact.id message.mailing_id = self.id message.change_status(:ready) message.delay.deliver end def deliver Rails.logger.info "Starting to Process Mailing '#{subject}' ID:#{id}" Lock.with_lock("mail_mgr_mailing_send[#{id}]") do |lock| unless status.to_s.eql?('scheduled') raise Exception.new("Mailing was not scheduled when job tried to run!") end unless scheduled_at <= Time.now Rails.logger.info "Mailing is not scheduled to run until #{scheduled_at} rescheduling job!" self.delay(run_at: scheduled_at).deliver return true end change_status(:processing) initialize_messages messages.pending.each do |message| if reload.status.to_s != 'processing' Rails.logger.warn "Mailing #{id} is no longer in processing status it was changed to #{status} while running" return false end begin # use the cached mailing parts, set messages mailing to self message.mailing=self message.change_status(:processing) message.deliver message.change_status(:sent) rescue => e message.result = "Error: #{e.message} - #{e.backtrace.join("\n")}" message.change_status(:failed) end Rails.logger.debug "Sleeping #{MailManager.sleep_time_between_messages} before next message" sleep MailManager.sleep_time_between_messages end change_status(:completed) if status.to_s.eql?('processing') end end def mailable return @mailable if @mailable return self unless mailable_type and mailable_id @mailable = mailable_type.constantize.find(mailable_id) end def self.cleanse_source(source) require 'iconv' unless String.method_defined?(:encode) if String.method_defined?(:encode) source.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') source.encode!('UTF-8', 'UTF-16') else ic = Iconv.new('UTF-8', 'UTF-8//IGNORE') source = ic.iconv(source) end end def self.substitute_values(source,substitutions) substitutions.each_pair do |substitution,value| if value.blank? source.gsub!(/##{substitution}#([^#]*)#/,'\1') rescue source = self.cleanse_source(source).gsub(/##{substitution}#([^#]*)#/,'\1') else source.gsub!(/##{substitution}#[^#]*#/,value.to_s) rescue source = self.cleanse_source(source).gsub(/##{substitution}#[^#]*#/,value.to_s) end end if defined? MailManager::ContactableRegistry.respond_to?(:valid_contactable_substitutions) MailManager::ContactableRegistry.valid_contactable_substitutions. reject{|key| substitutions.keys.include?(key)}.each do |substitution| source.gsub!(/##{substitution}#([^#]*)#/,'\1') rescue source = self.cleanse_source(source).gsub(/##{substitution}#([^#]*)#/,'\1') end end source end def raw_parts @raw_parts ||= mailable.mailable_parts end def parts(substitutions={}) parts = [] raw_parts.each do |type,source| parts << [type, Mailing.substitute_values(source.dup,substitutions)] end parts end def mailable=(value) return if value.nil? self[:mailable_type] = value.class.name self[:mailable_id] = value.id end def mailable_class_and_id=(value) return if value.nil? parts = value.split(/_/) self[:mailable_id] = parts.pop self[:mailable_type] = parts.join('_') end #def mailable_attributes=(mailable_attributes={}) # mailable_attributes.each_pair do |key,value| # end #end # creates all of the Messages that will be sent for this mailing def initialize_messages unless messages.length > 0 Rails.logger.info "Building mailing messages for mailing(#{id})" transaction do emails_hash = messages.select{|m| m.type.eql?('MailManager::Message')}.inject(Hash.new){|emails_hash,message| emails_hash.merge(Mailing.clean_email_address(message.email_address)=>1)} mailing_lists.each do |mailing_list| mailing_list.subscriptions.active.each do |subscription| contact = subscription.contact next if contact.nil? or contact.deleted? email_address = Mailing.clean_email_address(contact.email_address) if emails_hash.has_key?(email_address) Rails.logger.info "Skipping duplicate address: #{email_address}" else Rails.logger.info "Adding #{email_address} to mailing #{subject}" emails_hash[email_address] = 1 message = Message.new message.subscription = subscription message.contact = contact message.mailing = self message.save end end end end save end end # clean up an email address for sending FIXME - maybe do a bit more def self.clean_email_address(email_address) email_address.downcase.strip end # sends a test message for this mailing to the given address def send_test_message(test_email_addresses) test_email_addresses.split(/,/).each do |test_email_address| puts "Creating test message for #{test_email_address}" test_message = TestMessage.new(:test_email_address => test_email_address.strip) test_message.mailing_id = self.id test_message.save test_message.delay.deliver end end # used in select helpers to identify this Mailing's Mailable def mailable_thing_and_id return '' if mailable.nil? return "#{mailable.class.name}_#{mailable.id}" end def mailing_list_ids=(mailing_list_ids) mailing_list_ids.delete('') self.mailing_lists = mailing_list_ids.collect{|mailing_list_id| MailingList.find_by_id(mailing_list_id)} end def can_pause? ['processing'].include?(status.to_s) end def can_edit? ['pending','scheduled','paused'].include?(status.to_s) end def can_cancel? ['pending','scheduled','processing','paused','resumed'].include?(status.to_s) end def can_resume? ['paused'].include?(status.to_s) end def can_schedule? ['pending'].include?(status.to_s) end def schedule raise "Unable to schedule" unless can_schedule? change_status('scheduled') delay(run_at: scheduled_at).deliver end def cancel raise "Unable to cancel" unless can_cancel? change_status('pending') # Delayed::Job.active.find(:all, :conditions => ["handler like ?","MailMgr::Mailing"]) #Changing this to return only the jobs that match the id so I don't have to parse with YAML ... seems logical mailing_jobs = Delayed::Job.find(:all, :conditions => ["handler like ?","%MailMgr::Mailing%"] || ["handler like ?", "%id: {job_mailing_id.to_i}\n%"]) #mailing_jobs = Delayed::Job.active.find(:all, :conditions => ["handler like ?","%MailMgr::Mailing%"]) mailing_jobs.each do |job| #job_mailing_id = YAML::load(job.handler).object.split(':').last #logger.debug "Job mailing id: #{job_mailing_id} - This mailing id: #{self.id} - do they match: #{job_mailing_id.to_i == self.id.to_i}" job.destroy #if job_mailing_id.to_i == self.id.to_i end end def resume raise "Unable to resume" unless can_resume? change_status('resumed') end def pause raise "Unable to pause" unless can_pause? change_status('paused') end end end