# Mailing lists are what make Sugoi-Mail go. Which makes sense, I suppose, # since Sugoi-Mail is a mailing list manager. # # A Mailing List has the following structure: # # (from the schema) # # name: The name of the mailing list. This will # become the local part of the list's email # address. # description:: The description of the mailing list. This # go into the "real name" portion of the mailing # list's email address listed in the "From:" # header in messages that are sent out to # subscribers. # user:: The Sugoi-Mail user who owns and manages the # mailing list--this is a User object. The domain # of the mailing list is the domain that the user # is a member of. # mailinglist_class:: The kind of mailing list that this is. This is # a MailinglistClass object. # welcome_admin_message:: The message that is sent out to a new member # when a subscription is received. This is an # AdminMessage object. # confirmed_admin_message:: The message that is sent out to a new member # when the subscription confirmation message # is confirmed. This is also an AdminMessage # object. # sayonara_admin_message_id:: The final AdminMessage object (as of the moment # anyway--couriermlm mailing lists have many more # administrative messages, and I suspect I may # have to implement all those at some point). # This is the message that is sent out to a # subscriber when she unsubscribes from the # mailing list. class Mailinglist < ActiveRecord::Base belongs_to :user has_and_belongs_to_many :addresses has_many :messages has_many :proxy_links validates_each :name, :on => :create do |record, attr, value| if record.user if Mailinglist.find_by_sql( ['select * from mailinglists m, users u where u.id = m.user_id and u.domain_id = ? and m.name = ?', record.user.domain_id, value ]).length > 0 then record.errors.add("name", "Address already exists: #{record.address}") end end end validates_associated :user, :on => :create validates_presence_of :user belongs_to :mailinglist_class # Since rails's has_one interface apparently DOESN'T WORK, I # have to do all these by hand. %w{welcome confirmed sayonara}.each do |msgtype| eval <<-EOT def #{msgtype}_admin_message AdminMessage.find #{msgtype}_admin_message_id end def #{msgtype}_admin_message=(msg) self.#{msgtype}_admin_message_id = msg.id end EOT end # Returns true if this mailing list's address is the canonical address of a # User. def canonical_address? id == user.mailinglist_id end # Returns true if the address +addr+ is subscribed to this mailing list. def has_address? addr expand_addresses.map { |a| a.address }.member? addr end def before_save welcome_admin_message ||= AdminMessage.find SysConfig.default_welcome_admin_message_id confirmed_admin_message ||= AdminMessage.find SysConfig.default_confirmed_admin_message_id sayonara_admin_message ||= AdminMessage.find SysConfig.default_farewell_admin_message_id # welcome_admin_message ||= AdminMessage.find 1 # XXX MAGIC CONSTANT # confirmed_admin_message ||= AdminMessage.find 2 # XXX MAGIC CONSTANT # sayonara_admin_message ||= AdminMessage.find 3 # XXX MAGIC CONSTANT end #------------------------------------------------------------------------ # Passthroughs to the mailinglist_class #------------------------------------------------------------------------ [ :public?, :closed?, :moderated?, :confirmation?, :joinable?, :archived?, :proxify? ].each do |m| define_method m do mailinglist_class.send m end end #------------------------------------------------------------------------ # These are all to make it work with rumble (as a replacement for # rumble's mailing-list class). #------------------------------------------------------------------------ def address name + "@" + user.domain.name # "#{name}@#{user.domain.name}" end def bounceaddress name + SysConfig.address_separator + SysConfig.bounce_suffix + "@" + user.domain.name # "#{name}-bounce@#{user.domain.name}" # XXX MAGIC CONSTANT XXX end def requestaddress name + SysConfig.address_separator + SysConfig.request_suffix + "@" + user.domain.name # "#{name}-request@#{user.domain.name}" # XXX MAGIC CONSTANT XXX end def self.find_by_address addr if ml = find_by_basic_address(addr) then return [ml,:mail] elsif info = find_by_proxy_address(addr) then ml, address_id = info return [ml, :proxy, address_id] elsif(ml = find_by_bounces_address(addr)) then return [ml, :bounces] elsif(ml = find_by_request_address(addr)) then return [ml, :request] end end def self.find_by_basic_address addr (ml_name, domain_name) = addr.split "@" domain=Domain.find_by_name domain_name if domain ml=Mailinglist.find_all_by_name(ml_name).find do |ml| ml.domain.id == domain.id end end end def self.find_by_proxy_address addr if matchdata=addr.match(/#{Regexp.escape(SysConfig.address_separator)}([0-9]+)\@/) then proxy_id=matchdata[1].to_i if ml=find_by_basic_address(addr.sub(/-[0-9]+\@/,"@")) then begin proxy=ProxyLink.find(proxy_id) return ml, proxy.address rescue nil end end end end def self.find_by_bounces_address bounces_addr find_by_basic_address bounces_addr.sub(/-bounces\@/,"@") end def self.find_by_request_address request_addr find_by_basic_address request_addr.sub(/-request\@/,"@") end def confirmed? addr if confirmation? then Confirmationcode.confirmed? self.id, addr.id else true end end def confirmed_addresses addresses.find_all { |addr| confirmed? addr } end def pending_addresses addresses.find_all { |addr| not confirmed? addr } end alias unconfirmed_addresses pending_addresses def expand_addresses address_list = addresses expanded = false until expanded foundsome = false new_address_list = address_list.map do |addr| if ml = Mailinglist.find_by_address(addr.address) then foundsome = true ml[0].addresses else addr end end.flatten expanded = ! foundsome address_list = new_address_list end address_list end def domain; user.domain; end def store_message message; messages << message; end def send_message message message.deliver end def subscribe(address,confirmationcode = nil) addr=Address.find_or_create_by_address(address) if confirmation? if confirmed_addresses.member? addr true else if confirmationcode then confirm addr, confirmationcode else send_welcome_message(addr) addresses << addr end end else if addresses.member? addr true else addresses << addr end end end def send_welcome_message(address) if Address === address then addr = address else addr=Address.find_or_create_by_address(address) end c=Confirmationcode.find_by_address_id_and_mailinglist_id addr.id, id unless c c=Confirmationcode.new c.address_id=addr.id c.mailinglist_id=id c.save end unless c.confirmed? welcome_admin_message.send_mail bounceaddress, addr.address, :address => addr.address, :command => "subscribe #{addr.address} #{c.code}", :domain => domain.name, :name => name, :description => description, :requestaddress => requestaddress end end def confirm address,confirmationcode addr=nil if Address === address addr=address else addr=Address.find_by_address(address) end if addr then if confirmation? then if Confirmationcode.confirm(self, addr, confirmationcode) then save confirmed_admin_message.send_mail bounceaddress, addr.address, :address => addr.address, :domain => domain.name, :name => name, :description => description, :requestaddress => requestaddress return true else return false end else return true end end end def unsubscribe(address, send_mail = true) addr=nil unless Address === address addr=Address.find_by_address address else addr=address end if addresses.member? addr then if send_mail sayonara_admin_message.send_mail bounceaddress, addr.address, :address => addr.address, :domain => domain.name, :name => name, :description => description, :requestaddress => requestaddress end remove_addresses addr end end def handle_request requestmessage subject=requestmessage.headers["Subject"][0].contents matches=requestmessage.body.match(/^\W*subscribe\s+(\S+@[a-z0-9.-]+) (?:\s+([a-z0-9]{16}))?$/x) if matches then # puts "Found subscription request: #{matches[0]}" addresscandidate=matches[1] confirmationcode=matches[2] # returns nil if there isn'tone # Since you can have mailing lists that require confirmation, but # they're not joinable, check for the confirmationcode case first. if confirmationcode then return subscribe(addresscandidate, confirmationcode) else if joinable? then return subscribe(addresscandidate) end end end # TODO: actually implement confirmed unsubscribing down in the # model. (This is, of course, to prevent pranksters from # unsubscribing you from a mailing list behind your back.) matches=requesmessage.body.match(/^\W*unsubscribe\s+(\S+@[a-z0-9.-]+) (?:\s+([a-z0-9]{16}))?$/x) if matches then addresscandidate=matches[1] confirmationcode=matches[2] return unsubscribe(addresscandidate, confirmationcode) end end end