#
# Copyright (C) 2002-2004 Satoru Takabayashi <satoru@namazu.org> 
# Copyright (C) 2003-2005 Kouichirou Eto
#     All rights reserved.
#     This is free software with ABSOLUTELY NO WARRANTY.
#
# You can redistribute it and/or modify it under the terms of 
# the GNU General Public License version 2.
#

require "kconv"
$LOAD_PATH.unshift("../../compat") unless $LOAD_PATH.include?("../../compat")
require "base64"
$LOAD_PATH << "../../lib" unless $LOAD_PATH.include?("../../lib")
require "qwik/ml-utils"
require "qwik/ml-mailsender"

module QuickML
  class Mail
    def initialize
      @mail_from = nil
      @recipients = []
      @header = []
      @body = ""
      @charset = nil
      @content_type = nil
      @bare = nil
    end
    attr_reader :recipients
    attr_reader :charset
    attr_reader :content_type
    attr_accessor :mail_from
    attr_accessor :body
    attr_accessor :bare

    def to_s
      str = ""
      each_field {|key, value| str << sprintf("%s: %s\n", key, value) }
      str << "\n"
      str << @body
      str
    end

    def parts
      #parts = @body.split(/^--#{Regexp.escape(self.boundary)}\n/)
      parts = @body.split(/^--#{Regexp.escape(self.boundary)}-*\n/)
      parts.shift  # Remove the first empty string.
      parts
    end

    def add_recipient (address)
      @recipients.push(normalize_address(address))
    end

    def clear_recipients
      @recipients = []
    end

    def [] (key)
      field = @header.find {|field| key.downcase == field.first.downcase}
      if field then field.last else "" end
    end

    def unshift_field (key, value)
      field = [key, value]  # Use Array for preserving order of the header
      @header.unshift(field)
    end

    def push_field (key, value)
      field = [key, value]  # Use Array for preserving order of the header
      @header.push(field)
    end

    def concat_field (value)
      lastfield = @header.last
      @header.pop
      push_field(lastfield.first, lastfield.last + "\n" + value)
    end

    def each_field
      @header.each {|field|
	yield(field.first, field.last)
      }
    end

    def looping?
      !self["X-QuickML"].empty?
    end

    def from
      address = if not self["From"].empty?
		  collect_address(self["From"]).first
		else
		  @mail_from
		end
      address = "unknown" if address.nil? or address.empty?
      normalize_address(address)
    end

    def collect_cc
      if self["Cc"]
	collect_address(self["Cc"])
      else
	[]
      end
    end

    def collect_to
      if self["To"]
	collect_address(self["To"])
      else
	[]
      end
    end

    def valid?
      (not @recipients.empty?) and @mail_from
    end

    def empty_body?
      return false if @body.length > 100
      /\A[\s　]*\Z/ =~ @body.tosjis # including Japanese zenkaku-space.
    end

    def multipart?
      %r!^multipart/mixed;\s*boundary=("?)(.*)\1!i =~ self["Content-Type"] #"
    end

    def boundary
      if %r!^multipart/mixed;\s*boundary=("?)(.*)\1!i =~ self["Content-Type"]#"
	$2
      else
	nil
      end
    end

    def read (string)
      header, body = string.split(/\n\n/, 2)
      attr = nil
      header.split("\n").each {|line|
	line.xchomp!
	if /^(\S+):\s*(.*)/=~ line
	  attr = $1
	  push_field(attr, $2)
	elsif attr
	  concat_field(line)
	end
      }
      @bare = string
      @charset = get_charset
      @content_type = get_content_type
      @body = (body or "")
    end

    # by eto

    def get_clean_subject
      str = Mail.clean_subject(self["Subject"])
      str.sub!(/(?:Re:\s*)+/i, "")
      str.sub!(/^\s+/, "")
      str.sub!(/\s+$/, "")
      str
    end

    def decoded_body
      encoding = self["Content-Transfer-Encoding"]

      if /base64/i =~ encoding
	return Base64.decode64(@body)
      end

      if /x-uuencode/i =~ encoding
	uu = ""
	@body.each_line{|line|
	  next if /\Abegin/ =~ line
	  break if /\Aend/ =~ line
	  uu << line
	}
	return uu.unpack("u").first
      end

      if /quoted-printable/i =~ encoding
	return qp_decode(@body)
      end

      @body
    end

    def qp_decode(str)
      str.gsub(/[ \t]+$/no, "").gsub(/=\r?\n/no, "").
	gsub(/=([0-9A-F][0-9A-F])/no) { $1.hex.chr }
    end

    def filename
      disp = self["Content-Disposition"]
      return nil if disp.nil?
      type, fdesc = disp.split(";")
      fdesc = fdesc.strip
      #qp type, fdesc
      #if /\Aattachment;\s+filename=\"(.*)\"\Z/ =~ disp # should use \z ?
      if /\Afilename=\"(.*)\"\z/ =~ fdesc
	filename = $1.jistosjis
	return filename
      end
      return nil
    end

    def plain_text_body?
      self["Content-Type"] == "" or
	%r!\btext/plain\b!i =~ self["Content-Type"] #/
    end

    def get_body
      body = ""
      self.each_part {|mail|
	content_type = mail.content_type
	if mail.plain_text_body?
	  # This code is too ad hoc.
	  body << mail.body.chomp.chomp+"\n"
	 #body << mail.decoded_body.chomp.chomp+"\n"
	end
      }
      body
    end

    def each_part(&block)
      if multipart?
	self.parts.each {|str|
	  submail = Mail.new
	  submail.read(str)
	  submail.each_part(&block) # recursive
	}
      else
	yield(self)
      end
    end

    private

    def get_content_type
      if %r!([-\w]+/[-\w]+)! =~ self["Content-Type"]
	$1.downcase
      else
	nil
      end
    end

    def get_charset
      if /charset=("?)([-\w]+)\1/ =~ self["Content-Type"]
	$2.downcase
      else
	nil
      end
    end

    def remove_comment_in_field (field)
      field = field.tosjis
      true while field.sub!(/\([^()]*?\)/, "")
      field
    end

    # foo@QuickML.CoM => foo@quickml.com
    # "foo"@example.com => foo@example.com
    def normalize_address (address)
      name, domain = address.split("@")
      name.gsub!(/^"(.*)"$/, '\1')
      if domain
	name + "@" + domain.downcase
      else
	address
      end
    end

    def collect_address (field)
      address_regex = 
	/(("?)[-0-9a-zA-Z_.+?\/]+\2@[-0-9a-zA-Z]+\.[-0-9a-zA-Z.]+)/ #/
      addresses = []
      parts = remove_comment_in_field(field).split(",")
      parts.each {|part|
	if (/<(.*?)>/ =~ part) or (address_regex =~ part)
	  addresses.push(normalize_address($1))
	end
      }
      addresses.uniq
    end

    public

    def self.send_mail (smtp_host, smtp_port, logger, optional = {})
      mail_from = optional[:mail_from]
      recipients = optional[:recipients]
      header = optional[:header]
      body = optional[:body]
      if optional[:recipient]
	raise unless optional[:recipient].kind_of?(String)
	recipients = [optional[:recipient]] 
      end
      raise if mail_from.nil? or recipients.nil? or 
	body.nil? or header.nil?

      contents = ""
      header.each {|field|
	key = field.first; value = field.last
	contents << "#{key}: #{value}\n" if key.kind_of?(String)
      }
      contents << "\n"
      contents << body
      begin
	sender = MailSender.new(smtp_host, smtp_port, true)
	sender.send(contents, mail_from, recipients)
      rescue => e
	logger.log "Error: Unable to send mail: #{e.class}: #{e.message}"
      end
    end

    def self.address_of_domain? (address, domain)
      domainpat = Regexp.new("[.@]" + Regexp.quote(domain) + "$",  #'
			     Regexp::IGNORECASE)
      if domainpat =~ address then true else false end
    end

    def self.encode_field (field)
      field.gsub(Regexp.new("[\x81\x40-\xea\xa2]\\S*\\s*", nil, "s")) {|x|
	x.scan(Regexp.new(".{1,10}", nil, "s")).map {|y|
	  "=?iso-2022-jp?B?" + y.tojis.to_a.pack("m").chomp + "?= "
	}.join("\n ")
      }
    end

    def self.decode_subject (subject)
      NKF.nkf("-s", subject.gsub(/\n\s*/, " "))
    end

    def self.clean_subject (subject)
      subject = Mail.decode_subject(subject)
      subject.gsub!(/(?:\[[^\]]+:\d+\])/, "")
      subject.sub!(/(?:Re:\s*)+/i, "Re: ")
      return subject
    end

    def self.rewrite_subject (subject, name, count)
      subject = Mail.clean_subject(subject)
      subject = "[#{name}:#{count}] " + subject
      Mail.encode_field(subject)
    end

    def self.join_parts (parts, boundary)
      body = ""
      body << sprintf("--%s\n", boundary)
      body << parts.join("--#{boundary}\n")
      body
    end

  end
end

# TODO: Make special mode for Mr. Misaki. Use the first line as Subject.

if $0 == __FILE__
  require "test/unit"
  require "qwik/ml-utils"
  require "qwik/util-string"
  require "qwik/util-kconv"
  require "qwik/qp"
  $debug = true
end

if $debug
  class TestMLMail < Test::Unit::TestCase
    def ml_mail(mail_from, add_recipient, body)
      mail = QuickML::Mail.new
      mail.mail_from = mail_from
      mail.add_recipient(add_recipient)
      mail.read(body.sjistojis)
      return mail
    end

    def test_mail
      str = <<"EOT"
From: "Test User" <user@e.com>
To: "Test Mailing List" <test@example.com>
Subject: Re: [test:1] Test Mail
Date: Mon, 3 Feb 2001 12:34:56 +0900

This is a test.
EOT
      mail = ml_mail("user@e.com", "test@example.com", str)

      assert_equal(["test@example.com"], mail.recipients)
      assert_equal(nil, mail.charset)
      assert_equal(nil, mail.content_type)
      assert_equal("user@e.com", mail.mail_from)
      assert_equal("This is a test.\n", mail.body)
      assert_equal(str, mail.bare)
      assert_equal("Re: [test:1] Test Mail", mail["Subject"])
      assert_equal(nil, mail.boundary)
      assert_equal("Test Mail", mail.get_clean_subject)
      assert_equal("This is a test.\n", mail.get_body)
      assert_equal("Mon, 3 Feb 2001 12:34:56 +0900", mail["Date"])
      assert_equal("t", QuickML::Mail.encode_field("t"))
      assert_equal(" Re: Test ",
		   QuickML::Mail.clean_subject("[test:1] Re: Test "))
      assert_equal(true, mail.plain_text_body?)
    end

    def test_multipart
      str = <<"EOT"
Date: Sun, 01 Aug 2004 12:34:56 +0900
From: Test User <user@e.com>
To: test@example.com
Subject: multipart test
Cc: guest@example.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="boundary"
Content-Transfer-Encoding: 7bit

--boundary
Content-Type: text/plain; charset="ascii"
Content-Transfer-Encoding: 7bit

test1

--boundary
Content-Type: text/plain; charset="ascii"
Content-Transfer-Encoding: 7bit

test2

--boundary--
EOT
      mail = ml_mail("user@e.com", "test@example.com", str)

      assert_equal(0, mail.multipart?)
      assert_equal(nil, mail.plain_text_body?)

      # test parts
      mail0 = QuickML::Mail.new
      mail0.read(mail.parts[0])
      assert_equal("test1\n\n", mail0.body)
      assert_equal(0, mail0.plain_text_body?)

      mail1 = QuickML::Mail.new
      mail1.read(mail.parts[1])
      assert_equal("test2\n\n", mail1.body)
      assert_equal(0, mail1.plain_text_body?)
    end

    def test_mail_with_image
      str = <<"EOT"
Date: Sun, 01 Aug 2004 12:34:56 +0900
From: Test User <user@e.com>
To: test@example.com
Subject: Attach Test
Cc: guest@example.com
Message-Id: <nosuchmessageid@example.com>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="------_410DDC04C7AD046D3600_MULTIPART_MIXED_"
Content-Transfer-Encoding: 7bit

--------_410DDC04C7AD046D3600_MULTIPART_MIXED_
Content-Type: text/plain; charset="ascii"
Content-Transfer-Encoding: 7bit

This is a test.

--------_410DDC04C7AD046D3600_MULTIPART_MIXED_
Content-Type: image/png; name="1x1.png"
Content-Disposition: attachment;
 filename="1x1.png"
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4//8/AAX+Av4zEpUU
AAAAAElFTkSuQmCC

--------_410DDC04C7AD046D3600_MULTIPART_MIXED_--
EOT
      mail = ml_mail("user@e.com", "test@example.com", str)

      assert_equal("multipart/mixed", mail.content_type)
      assert_equal("------_410DDC04C7AD046D3600_MULTIPART_MIXED_",
		   mail.boundary)
      assert_equal(2, mail.parts.length)
      assert_equal(nil, mail.plain_text_body?)

      mail0 = QuickML::Mail.new
      mail0.read(mail.parts[0])
      assert_equal("text/plain", mail0.content_type)
      assert_equal("This is a test.\n\n", mail0.body)
      assert_equal(0, mail0.plain_text_body?)

      mail1 = QuickML::Mail.new
      mail1.read(mail.parts[1])
      assert_equal(nil, mail1.plain_text_body?)
      assert_equal("image/png", mail1.content_type)
      #assert_equal(0, mail1.base64?)
      assert_equal("attachment;\n filename=\"1x1.png\"",
		   mail1["Content-Disposition"])
      assert_equal("1x1.png", mail1.filename)
      assert_equal("base64", mail1["Content-Transfer-Encoding"])
      assert_equal("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4//8/AAX+Av4zEpUU\nAAAAAElFTkSuQmCC\n\n", mail1.body)
      assert_equal("\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\001\000\000\000\001\010\002\000\000\000\220wS\336\000\000\000\fIDATx\332c\370\377\377?\000\005\376\002\3763\022\225\024\000\000\000\000IEND\256B`\202", mail1.decoded_body)

      assert_equal(nil, mail.parts[2])
    end

    def test_mail_uuencoded
      str = <<'EOT'
Date: Sun, 01 Aug 2004 12:34:56 +0900
From: Test User <user@e.com>
To: test@example.com
Subject: UuencodeImage
Message-Id: <nosuchmessageid@example.com>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="------------Boundary_iGrnGAlmY.rxNIm"
X-Mailer: EdMax Ver2.85.5F

--------------Boundary_iGrnGAlmY.rxNIm
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: 7bit

This is a test.

--------------Boundary_iGrnGAlmY.rxNIm
Content-Type: application/octet-stream; name="1x1a.png"
Content-Transfer-Encoding: x-uuencode
Content-Disposition: attachment; filename="1x1a.png"

begin 666 1x1a.png
MB5!.1PT*&@H````-24A$4@````$````!"`(```"0=U/>````#$E$051XVF/X
8__\_``7^`OXS$I44`````$E%3D2N0F""
`
end
EOT
      mail = ml_mail("user@e.com", "test@example.com", str)

      # test_mail
      mail0 = QuickML::Mail.new
      mail0.read(mail.parts[0])
      assert_equal("text/plain", mail0.content_type)
      assert_equal("This is a test.\n\n", mail0.body)

      mail1 = QuickML::Mail.new
      mail1.read(mail.parts[1])
      assert_equal("application/octet-stream", mail1.content_type)
      #assert_equal(0, mail1.uuencode?)
      assert_equal("attachment; filename=\"1x1a.png\"",
		   mail1["Content-Disposition"])
      assert_equal("1x1a.png", mail1.filename)
      assert_equal("x-uuencode", mail1["Content-Transfer-Encoding"])
      uu = "MB5!.1PT*&@H````-24A\$4@````\$````!\"`(```\"0=U/>````#\$E\$051XVF/X\n8__\\_``7^`OXS\$I44`````\$E%3D2N0F\"\"\n"
      assert_equal("begin 666 1x1a.png\n#{uu}`\nend\n", mail1.body)
      assert_equal("\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\001\000\000\000\001\010\002\000\000\000\220wS\336\000\000\000\fIDATx\332c\370\377\377?\000\005\376\002\3763\022\225\024\000\000\000\000IEND\256B`\202", mail1.decoded_body)
      assert_equal(nil, mail.parts[2])
    end

    def test_mail_quoted_printable
      str = <<'EOT'
From user@gmail.com Sat Apr 02 18:43:00 2005
Date: Sat, 2 Apr 2005 18:43:00 +0900
From: user <user@gmail.com>
To: test@example.com
Mime-Version: 1.0
Content-Type: text/plain; charset=ISO-2022-JP
Content-Transfer-Encoding: quoted-printable
Subject: [qwik-users:217] Gmail=?iso-2022-jp?B?GyRCJCwbKEJKSVMbJEIlKCVzJTMhPCVHJSMbKEI=?= 
 =?iso-2022-jp?B?GyRCJXMlMCRLO0VNTUpROTkbKEI=?= 
Reply-To: test@example.com
X-Mail-Count: 217
Precedence: bulk
X-ML-Address: test@example.com
X-ML-Name: test
X-ML-Info: http://qwik.jp/
X-QuickML: true

--=20
user
EOT
      mail = ml_mail("user@gmail.com", "test@example.com", str)
      assert_equal("text/plain", mail.content_type)
      assert_equal("--=20\nuser\n", mail.body)
      assert_equal("-- \nuser\n".sjistojis, mail.decoded_body)
      assert_equal("quoted-printable", mail["Content-Transfer-Encoding"])
    end

    def test_base64
      str = <<"EOT"
Date: Thu, 14 Apr 2005 21:22:30 +0900
From: user@e.com
To: test@example.com
User-Agent: Wanderlust/2.10.0 (Venus) SEMI/1.14.4 (Hosorogi) FLIM/1.14.4
 (=?ISO-8859-4?Q?Kashiharajing=FE-mae?=) APEL/10.4 Emacs/21.3
 (i386-pc-linux-gnu) MULE/5.0 (SAKAKI)
MIME-Version: 1.0 (generated by SEMI 1.14.4 - "Hosorogi")
Content-Type: multipart/mixed;
 boundary="Multipart_Thu_Apr_14_21:22:30_2005-1"
Subject: This is an inline test.

--Multipart_Thu_Apr_14_21:22:30_2005-1
Content-Type: text/plain; charset=ISO-2022-JP

サンプル.jpgを添付してみます。
--Multipart_Thu_Apr_14_21:22:30_2005-1
Content-Type: image/jpeg
Content-Disposition: inline; filename="sample.jpg"
Content-Transfer-Encoding: base64

iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4//8/AAX+Av4zEpUU
AAAAAElFTkSuQmCC

EOT
      mail = ml_mail("user@e.com", "test@example.com", str)

      assert_equal("multipart/mixed", mail.content_type)
      #assert_equal("------_410DDC04C7AD046D3600_MULTIPART_MIXED_",
      assert_equal("Multipart_Thu_Apr_14_21:22:30_2005-1",
		   mail.boundary)
      assert_equal(2, mail.parts.length)
      assert_equal(nil, mail.plain_text_body?)

      mail0 = QuickML::Mail.new
      mail0.read(mail.parts[0])
      assert_equal("text/plain", mail0.content_type)
      # $KCODE = "s"
      #assert_equal("This is a test.\n\n", mail0.body)
      assert_equal(0, mail0.plain_text_body?)

      mail1 = QuickML::Mail.new
      mail1.read(mail.parts[1])
      assert_equal(nil, mail1.plain_text_body?)
      assert_equal("image/jpeg", mail1.content_type)
      assert_equal("inline; filename=\"sample.jpg\"",
		   mail1["Content-Disposition"])
      assert_equal("sample.jpg", mail1.filename)
      assert_equal("base64", mail1["Content-Transfer-Encoding"])
      assert_equal("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4//8/AAX+Av4zEpUU\nAAAAAElFTkSuQmCC\n\n", mail1.body)
      assert_equal("\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\001\000\000\000\001\010\002\000\000\000\220wS\336\000\000\000\fIDATx\332c\370\377\377?\000\005\376\002\3763\022\225\024\000\000\000\000IEND\256B`\202", mail1.decoded_body)

      assert_equal(nil, mail.parts[2])
    end

  end
end
