require "date" require "gurgitate/mailmessage" require "net/smtp" require "socket" # require "pp" # The Message class is one of the most important classes in # sugoi-mail--mainly because it *does* so much. But anyway. Herewith a # Model With Some Controller-like Things In It. # # This is the class that handles what a "message" is, and how it # behaves. It makes considerable use of gurgitate-mail's mail-parsing # library, so it might not be a bad idea to familiarize yourself with # that if trying to work on this. class Message < ActiveRecord::Base belongs_to :mailinglist belongs_to :address acts_as_tree :order => "timestamp" # has_and_belongs_to_many :mailinglists class << self private def get_or_create_messageid mess unless mess.headers["Message-Id"] mess.headers["Message-Id"] = "<"+`uuidgen || uuid`.chomp + "@" + Socket::gethostbyname("localhost")[0] + ">" end mess.headers["Message-Id"][0].contents end def get_or_create_timestamp mess if mess.headers["Date"] then DateTime.parse(mess.headers["Date"][0].contents) else Time.now end end def find_parent mess referenced=nil if mess.headers["In-Reply-To"] then referenced=mess.headers["In-Reply-To"][0].contents . match(/(\<[^>]+\>)/) if referenced then referenced=referenced[1] else if mess.headers["References"] then referenced=mess.headers["References"][0].contents. split.last end end end Message.find_by_messageid referenced end public # Processes a Gurgitate::Mailmessage object or the text of a # message and creates a new Message object. # # +mess+:: Either a Gurgitate::Mailmessage object (with envelope # information intact), or the text of a mail message (in which # case the envelope information must be supplied) # +recipient+:: The envelope recipient (SMTP's RCPT To:) of the mail # message. # +sender+:: The envelope sender (SMTP's MAIL From:) of the mail # message. def from_message(mess, recipient=nil, sender=nil) # This method is way too long, sorry if Gurgitate::Mailmessage === mess then message = self.new message.messageid = get_or_create_messageid mess message.timestamp = get_or_create_timestamp mess addr = Address.find_or_create_by_address mess.from if addr.new_record? then addr.save end message.address = addr message.headers = mess.headers.to_s message.body = mess.body.to_s message.parent = find_parent mess message.envelope_from = mess.from message.envelope_to = mess.to.map { |t| t.contents } message.address.save if message.address.new_record? ml, type = Mailinglist.find_by_address(message.envelope_to[0]) if type == :mail and ml then message.mailinglist = ml end return message elsif String === mess then from_message Gurgitate::Mailmessage.new(mess),recipient,sender end end alias :parse :from_message end private # Extracts the bare email addresses from a header, without the extra # commentary like the real names. def pull_out_addresses header if header header.contents.split(/,/).map do |addr| Address.parse(addr)[0] end else [ ] end end # Returns the proxified replacement for a header, with regular email # addresses replaced by proxy addresses. def proxify_header header, addr unless Mailinglist.find_by_address addr if proxy_address = Address.proxyaddress(addr) header[0] . contents . sub( Regexp.new(Regexp.escape(addr) ), proxy_address ) end end end def proxified_plain_addr header, addr matches = addr.match(/(.*?)#(.*?)@(.*)/) if matches then localpart, destdomain, msgs_domain = matches[1..3] to_address = "#{localpart}@#{destdomain}" if Domain.find_by_name(msgs_domain) then header[0] . contents . sub(Regexp.new(Regexp.escape(addr)), to_address) end end end def proxify_mail_to_virtual_users # special-case closed mail to mailing lists owned by virtual users if mailinglist if mailinglist.closed? if Mailinglist.find_by_address(envelope_to)[0].user. mailinglist.has_address? envelope_from then if mailinglist.user.description then @mess.headers["From"] = "=?UTF-8?B?" + [ mailinglist.user.description ].pack("m").chomp + "?= <" + mailinglist.user.address + ">" else @mess.headers["From"] = mailinglist.user.address end # @mess.headers.instance_variable_set("@headers_changed",true) end end end end def proxify_proxifiable_mailing_list addr=Address.find_or_create_by_address @mess.from if addr.new_record? then addr.save end if addr.outside? then if mailinglist if mailinglist.proxify? if @mess.headers["From"] then @mess.headers["Reply-To"] = addr.proxified mailinglist end end end end end def proxify_normal_addresses %w{From To Cc}.each do |header_name| if @mess.headers[header_name] then @mess.headers[header_name].map do |header| pull_out_addresses header end.flatten.each do |addr| if newhdr = proxify_header(@mess.headers[header_name], addr) @mess.headers[header_name] = newhdr end if newhdr = proxified_plain_addr(@mess.headers[header_name], addr) @mess.headers[header_name] = newhdr end end end end end # Rewrites the From:, To: and Cc: headers in the current message # to reflect mail proxy addresses instead of "real" addresses. def proxify_addresses @mess = Gurgitate::Mailmessage.new to_s proxify_normal_addresses proxify_mail_to_virtual_users proxify_proxifiable_mailing_list @mess.to_s end public # Returns the text of the message that would be sent out by the # "deliver" message. This might have headers rewritten to reflect # mail aliases. def messagetext(smtpdetails={}) if smtpdetails.length == 1 and smtpdetails[0].has_key? :custom_message smtpdetails[0][:custom_message] end if mailinglist.canonical_address? or mailinglist.closed? then proxify_addresses else to_s end end def proxy_deliver(from_address, to_address) text = proxify_addresses smtpserver = "localhost" smtpport = 25 Net::SMTP.start(smtpserver, smtpport, "localhost") do |smtp| smtp.send_message text, from_address, to_address end end # Delivers a message with the optional parameter hash +smtpdetails+. # # def deliver(*smtpdetails) if Hash === smtpdetails[0] then smtpdetails = smtpdetails[0] else smtpdetails = {} end if mailinglist.closed? if mailinglist.user.mailinglist.has_address? envelope_from or mailinglist.user.address == envelope_from then end unless mailinglist.user.mailinglist.has_address? self.envelope_from return bounce end end if mailinglist.confirmation? addresses = mailinglist.confirmed_addresses.map { |a| a.address } else addresses = mailinglist.expand_addresses.map { |a| a.address } end # Hm, not sure why this is *here* instead of somewhere # else. Fix later. # # (This should actually be in the database somewhere, I'm thinking.) smtpserver = SysConfig.smtpserver smtpport = SysConfig.smtpport mysmtpname = SysConfig.mysmtpname # smtpserver = "localhost" # XXX MAGIC CONSTANT XXX # smtpport = 25 # XXX MAGIC CONSTANT XXX # mysmtpname = "localhost" # XXX MAGIC CONSTNAT XXX if smtpdetails.has_key? :smtpserver then smtpserver = smtpdetails[:smtpserver] end if smtpdetails.has_key? :smtpport then smtpport = smtpdetails[:smtpport] || 25 end fromaddress = mailinglist.name + SysConfig.address_separator + SysConfig.bounce_suffix + "@" + mailinglist.user.domain.name # XXX MAGIC CONSTANT XXX # fromaddress = "#{mailinglist.name}-bounces@#{mailinglist.user.domain.name}" Net::SMTP.start(smtpserver, smtpport, mysmtpname) do |smtp| # puts "Will send email to #{addresses.inspect}" # puts "Message id is #{id}" smtp.send_message messagetext, fromaddress, addresses end end def bounce # TODO nil end # Returns the text of the current message def to_s headers.to_s.chomp.chomp+"\n\n"+body.to_s end alias responses children # "children" comes from acts_as_tree end