#!/usr/lib/ruby/bin/ruby

$:.unshift File.dirname(__FILE__) + '/../lib'

require 'optparse'
require 'rubygems'
require 'mapi/msg'
require 'mapi/pst'
require 'time'

class Mapitool
	attr_reader :files, :opts
	def initialize files, opts
		@files, @opts = files, opts
		seen_pst = false
		raise ArgumentError, 'Must specify 1 or more input files.' if files.empty?
		files.map! do |f|
			ext = File.extname(f.downcase)[1..-1]
			raise ArgumentError, 'Unsupported file type - %s' % f unless ext =~ /^(msg|pst)$/
			raise ArgumentError, 'Expermiental pst support not enabled' if ext == 'pst' and !opts[:enable_pst]
			[ext.to_sym, f]
		end
		if dir = opts[:output_dir]
			Dir.mkdir(dir) unless File.directory?(dir)
		end
	end

	def each_message(&block)
		files.each do |format, filename|
			if format == :pst
				if filter_path = opts[:filter_path]
					filter_path = filter_path.tr("\\", '/').gsub(/\/+/, '/').sub(/^\//, '').sub(/\/$/, '')
				end
				open filename do |io|
					pst = Mapi::Pst.new io
					pst.each do |message|
						next unless message.type == :message
						if filter_path
							next unless message.path =~ /^#{Regexp.quote filter_path}(\/|$)/i
						end
						yield message
					end
				end
			else
				Mapi::Msg.open filename, &block
			end
		end
	end

	def run
		each_message(&method(:process_message))
	end

	def make_unique filename
		@map ||= {}
		return @map[filename] if !opts[:individual] and @map[filename]
		try = filename
		i = 1
		try = filename.gsub(/(\.[^.]+)$/, ".#{i += 1}\\1") while File.exist?(try)
		@map[filename] = try
		try
	end

	def process_message message
		# TODO make this more informative
		mime_type = message.mime_type
		return unless pair = Mapi::Message::CONVERSION_MAP[mime_type]

		combined_map = {
			'eml' => 'Mail.mbox',
			'vcf' => 'Contacts.vcf',
			'txt' => 'Posts.txt'
		}

		# TODO handle merged mode, pst, etc etc...
		case message
		when Mapi::Msg
			if opts[:individual]
				filename = message.root.ole.io.path.gsub(/msg$/i, pair.last)
			else
				filename = combined_map[pair.last] or raise NotImplementedError
			end
		when Mapi::Pst::Item
			if opts[:individual]
				filename = "#{message.subject.tr ' ', '_'}.#{pair.last}".gsub(/[^A-Za-z0-9.()\[\]{}-]/, '_')
			else
				filename = combined_map[pair.last] or raise NotImplementedError
				filename = (message.path.tr(' /', '_.').gsub(/[^A-Za-z0-9.()\[\]{}-]/, '_') + '.' + File.extname(filename)).squeeze('.')
			end
			dir = File.dirname(message.instance_variable_get(:@desc).pst.io.path)
			filename = File.join dir, filename
		else
			raise
		end

		if dir = opts[:output_dir]
			filename = File.join dir, File.basename(filename)
		end

		filename = make_unique filename

		write_message = proc do |f|
			data = message.send(pair.first).to_s
			if !opts[:individual] and pair.last == 'eml'
				# we do the append > style mbox quoting (mboxrd i think its called), as it
				# is the only one that can be robuslty un-quoted. evolution doesn't use this!
				f.puts "From mapitool@localhost #{Time.now.rfc2822}"
				#munge_headers mime, opts
				data.lines.each do |line|
					if line =~ /^>*From /o
						f.print '>' + line
					else
						f.print line
					end
				end
			else
				f.write data
			end
		end

		if opts[:stdout]
			write_message[STDOUT]
		else
			open filename, 'a', &write_message
		end
	end

	def munge_headers mime, opts
		opts[:header_defaults].each do |s|
			key, val = s.match(/(.*?):\s+(.*)/)[1..-1]
			mime.headers[key] = [val] if mime.headers[key].empty?
		end
	end
end

def mapitool
	opts = {:verbose => false, :action => :convert, :header_defaults => []}
	op = OptionParser.new do |op|
		op.banner = "Usage: mapitool [options] [files]"
		#op.separator ''
		#op.on('-c', '--convert', 'Convert input files (default)') { opts[:action] = :convert }
		op.separator ''
		op.on('-o', '--output-dir DIR', 'Put all output files in DIR') { |d| opts[:output_dir] = d }
		op.on('-i', '--[no-]individual', 'Do not combine converted files') { |i| opts[:individual] = i }
		op.on('-s', '--stdout', 'Write all data to stdout') { opts[:stdout] = true }
		op.on('-f', '--filter-path PATH', 'Only process pst items in PATH') { |path| opts[:filter_path] = path }
		op.on(      '--enable-pst', 'Turn on experimental PST support') { opts[:enable_pst] = true }
		#op.on('-d', '--header-default STR', 'Provide a default value for top level mail header') { |hd| opts[:header_defaults] << hd }
		# --enable-pst
		op.separator ''
		op.on('-v', '--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] = v }
		op.on_tail('-h', '--help', 'Show this message') { puts op; exit }
	end

	files = op.parse ARGV

	# for windows. see issue #2
	STDOUT.binmode

	Mapi::Log.level = Ole::Log.level = opts[:verbose] ? Logger::WARN : Logger::FATAL

	tool = begin
		Mapitool.new(files, opts)
	rescue ArgumentError
		puts $!
		puts op
		exit 1
	end
	
	tool.run
end

mapitool

__END__

mapitool [options] [files]

files is a list of *.msg & *.pst files.

one of the options should be some sort of path filter to apply to pst items.

--filter-path=
--filter-type=eml,vcf

with that out of the way, the entire list of files can be converted into a
list of items (with meta data about the source).

--convert
--[no-]separate one output file per item or combined output
--stdout
--output-dir=.