Code:
#!/usr/bin/env ruby
require 'getoptlong'
require 'net/imap'
require 'thread'
require 'pp'
HELP_INFO = '
Synopsis:
This script will allow you to migrate IMAP account(s) from one server to the other.
Originaly was created by Ryan Grove and can be found at http://wonko.com/post/ruby_script_to_sync_email_from_any_imap_server_to_gmail
I have put in lots of my own modifications and made it more robust and flexible.
By: Ian Matyssik (2009)
Usage:
IMAPmigrator [options]
-h, --help :
show help
-s <server fqdn or IP>, --from-server <server fqdn or IP> :
Specify server name you would like to migrate from.
-p <port#>, --from-port <port#> :
Specify port number on "from" server to connect to.
Default: 143, 993 when --from-ssl is used.
--from-ssl :
Enable SSL when connect to "from" server.
-u <username>, --from-user <username> :
Specify to use when connectin to "from" server.
-x <password>, --from-pass <password> :
Password to use when connecting to "from" server.
--from-pass-file <full path to the file with password> :
If you do not want to show password on command line and would like to store password in the file.
Please make sure that file contains only one password for the specified user.
-S <server fqdn or IP>, --to-server <server fqdn or IP> :
Specify server name you would like to migrate to.
-P <port#>, --to-port <port#> :
Specify port number on "to" server to connect to.
Default: 143, 993 when --from-ssl is used.
--to-ssl :
Enable SSL when connect to "to" server.
-U <username>, --to-user <username> :
Specify to use when connectin to "to" server.
-X <password>, --to-pass <password> :
Password to use when connecting to "to" server.
--to-pass-file <full path to the file with password> :
If you do not want to show password on command line and would like to store password in the file.
Please make sure that file contains only one password for the specified user.
Filtering options:
--from-prefix <string>
If "from" server uses a prefix, please specify it here.
Example: --from-preifx INBOX
--from-delimiter <string>
If "from" server uses different delimiter from the "to" server, then I suggest you to specify both delimiters: "to" and "from"
Example: --from-delimiter "."
--to-prefix <string>
If "to" server uses a prefix, please specify it here.
Example: --to-preifx INBOX
--to-delimiter <string>
If "to" server uses different delimiter from the "from" server, then I suggest you to specify both delimiters: "to" and "from"
Example: --to-delimiter "/"
--msg-since-days <number of days> :
Specify number of days into the past since today you would like to filter messages on.
Only messages that have been received this many days ago will be analysed and transffered.
--msg-before-days <number of days> :
Specify number of days into the past since today you would like to filter messages on.
Only messages that have been received this many days before will be analysed and transffered.
--accepted-flags <comma separated list of accepted flags> :
Specify list of IMAP flags you would like to be synced in the following form:
"Deleted,Seen,Flagged,Answered"
Default is the following:
"Seen,Deleted,Answered,Draft,Flagged"
Be carefull, some flags like ":Recent" will cause some servers to puke.
Info:
Script is not perfect by any means, please look-out for changing of Mailbox names. I have put change from "/Junk" to "/Spam" staticaly in the script, if not needed please change to what you need or delete it.
Note: This script was originaly released under GPL and should stay the same.
Also you should know that this script comes with no guarantee and support.
If it breaks you mail, server, house, health, etc. it should be your own responcibility.
'
opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--from-server', '-s', GetoptLong::REQUIRED_ARGUMENT ],
[ '--from-port', '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '--from-ssl', GetoptLong::NO_ARGUMENT ],
[ '--from-user', '-u', GetoptLong::REQUIRED_ARGUMENT ],
[ '--from-pass', '-x', GetoptLong::REQUIRED_ARGUMENT ],
[ '--from-pass-file', GetoptLong::REQUIRED_ARGUMENT ],
[ '--from-prefix', GetoptLong::REQUIRED_ARGUMENT ],
[ '--from-delimiter', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-server', '-S', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-port', '-P', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-ssl', GetoptLong::NO_ARGUMENT ],
[ '--to-user', '-U', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-pass', '-X', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-pass-file', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-prefix', GetoptLong::REQUIRED_ARGUMENT ],
[ '--to-delimiter', GetoptLong::REQUIRED_ARGUMENT ],
[ '--msg-since-days', GetoptLong::REQUIRED_ARGUMENT ],
[ '--msg-before-days', GetoptLong::REQUIRED_ARGUMENT ],
[ '--accepted-flags', GetoptLong::REQUIRED_ARGUMENT ]
)
# Source server connection info.
source_host = ''
source_port = 0
source_ssl = false
source_user = ''
source_pass = ''
source_delimiter = ''
source_prefix = ''
# Destination server connection info.
dest_host = ''
dest_port = 0
dest_ssl = false
dest_user = ''
dest_pass = ''
dest_delimiter = ''
dest_prefix = ''
# Filtering options
search_criteria = Array.new
accepted_flags = Array.new
opts.each do |opt, arg|
case opt
when '--help'
print HELP_INFO
exit
when '--from-server'
source_host = arg.to_s.strip
when '--from-port'
source_port = arg.strip.to_i
when '--from-ssl'
source_ssl = true
when '--from-user'
source_user = arg.to_s.strip
when '--from-pass'
source_pass = arg.to_s.strip
when '--from-pass-file'
source_pass = file.read(arg.to_s).to_s.strip
when '--from-prefix'
source_prefix = arg.to_s.strip
when '--from-delimiter'
source_delimiter = arg.to_s.strip
when '--to-server'
dest_host = arg.to_s.strip
when '--to-port'
dest_port = arg.strip.to_i
when '--to-ssl'
dest_ssl = true
when '--to-user'
dest_user = arg.to_s.strip
when '--to-pass'
dest_pass = arg.to_s.strip
when '--to-pass-file'
dest_pass = file.read(arg.to_s).to_s.strip
when '--to-prefix'
dest_prefix = arg.to_s.strip
when '--to-delimiter'
dest_delimiter = arg.to_s.strip
when '--msg-since-days'
tmp_since = Time.now - (arg.to_i * 60 * 60 * 24)
search_criteria += [ 'SINCE' , tmp_since.strftime('%d-%b-%Y') ]
when '--msg-before-days'
tmp_before = Time.now - (arg.to_i * 60 * 60 * 24)
search_criteria += [ 'BEFORE', tmp_before.strftime('%d-%b-%Y') ]
when '--accepted-flags'
arg.to_s.split(%r{\s*,\s*}).each {|f| accepted_flags.push(:"#{f}")}
end
end
# Source server connection info.
if source_host.length == 0 then
puts "Please specify --from-host, I do not know where to connect to!"
exit
else
SOURCE_HOST = source_host
end
SOURCE_SSL = source_ssl
if source_ssl && source_port == 0 then
SOURCE_PORT = 993
elsif !source_ssl && source_port == 0 then
SOURCE_PORT = 143
else
SOURCE_PORT = source_port
end
if source_user.length == 0 then
puts "Please specify --from-user, I do not know whom to connect as!"
exit
else
SOURCE_USER = source_user
end
SOURCE_PASS = source_pass
SOURCE_DELIMITER = source_delimiter
SOURCE_PREFIX = source_prefix
# Destination server connection info.
if dest_host.length == 0 then
puts "Please specify --to-host, I do not know where to connect to!"
exit
else
DEST_HOST = dest_host
end
DEST_SSL = dest_ssl
if dest_ssl && dest_port == 0 then
DEST_PORT = 993
elsif !dest_ssl && dest_port == 0 then
DEST_PORT = 143
else
DEST_PORT = dest_port
end
if dest_user.length == 0 then
puts "Please specify --to-user, I do not know whom to connect as!"
exit
else
DEST_USER = dest_user
end
DEST_PASS = dest_pass
DEST_DELIMITER = dest_delimiter
DEST_PREFIX = dest_prefix
if search_criteria.length != 0 then
SEARCH_CRITERIA = search_criteria
else
SEARCH_CRITERIA = ['ALL']
end
if accepted_flags.length != 0 then
ACCEPTED_FLAGS = accepted_flags
else
ACCEPTED_FLAGS = [ :Seen, :Deleted, :Answered, :Draft, :Flagged ]
end
#Textual represantation of "from" and "to" names
SOURCE_NAME = SOURCE_USER
DEST_NAME = DEST_USER
#Number of secconds to sleep between NOOPs to the server
NOOP_INTERVAL = 180
# Maximum number of messages to select at once.
UID_BLOCK_SIZE = 512
# Utility methods.
def dd(message)
puts "[#{DEST_HOST}: #{DEST_NAME}] #{message}"
end
def ds(message)
puts "[#{SOURCE_HOST}: #{SOURCE_NAME}] #{message}"
end
def uid_fetch_block(server, uids, *args)
pos = 0
while pos < uids.size
server.uid_fetch(uids[pos, UID_BLOCK_SIZE], *args).each {|data| yield data }
pos += UID_BLOCK_SIZE
end
end
def server_send_noop(server,interval)
while true do
if !server.disconnected?() then
server.noop()
puts 'Sent NOOP to the server ...'
sleep interval
end
end
end
@failures = 0
@existing = 0
@synced = 0
# Connect and log into both servers.
ds 'Connecting...'
source = Net::IMAP.new(SOURCE_HOST, SOURCE_PORT, SOURCE_SSL)
ds 'Logging in...'
source.login(SOURCE_USER, SOURCE_PASS)
src_thread = Thread.new {server_send_noop(source,NOOP_INTERVAL)}
FOLDER_LIST = source.list("","*")
FOLDERS = Hash.new
FOLDER_LIST.each do |src_folder|
# Open source folder in read-only mode.
begin
ds "Selecting folder '#{src_folder.name}'..."
source.examine(src_folder.name)
source.subscribe(src_folder.name)
new_folder = src_folder.name.sub(/^#{SOURCE_PREFIX}#{SOURCE_DELIMITER}/,"#{DEST_PREFIX}#{SOURCE_DELIMITER}")
new_folder = new_folder.gsub(SOURCE_DELIMITER,DEST_DELIMITER)
# Remove me XXX
new_folder = new_folder.sub(/^\/Junk/,"/Spam")
FOLDERS[src_folder.name] = new_folder
rescue => e
ds "Error: select failed: #{e}"
if source.disconnected?() then
begin
source = Net::IMAP.new(SOURCE_HOST, SOURCE_PORT, SOURCE_SSL)
source.login(SOURCE_USER, SOURCE_PASS)
rescue => e
ds "Error: select failed: #{e}"
end
end
next
end
end
pp FOLDERS
##################################
dd 'Connecting...'
dest = Net::IMAP.new(DEST_HOST, DEST_PORT, DEST_SSL)
dd 'Logging in...'
dest.login(DEST_USER, DEST_PASS)
dst_thread = Thread.start {server_send_noop(dest,NOOP_INTERVAL)}
# Loop through folders and copy messages.
FOLDERS.each do |source_folder, dest_folder|
# Open source folder in read-only mode.
begin
ds "Selecting folder '#{source_folder}'..."
source.examine(source_folder)
rescue => e
ds "Error: select failed: #{e}"
next
end
# Open (or create) destination folder in read-write mode.
begin
dd "Selecting folder '#{dest_folder}'..."
dest.select(dest_folder)
rescue => e
begin
dd "Folder not found; creating..."
dest.create(dest_folder)
dest.select(dest_folder)
rescue => ee
dd "Error: could not create folder: #{e}"
next
end
end
# Build a lookup hash of all message ids present in the destination folder.
dest_info = {}
dd 'Analyzing existing messages...'
if SEARCH_CRITERIA.length == 0 then
uids = dest.uid_search(['ALL'])
else
uids = dest.uid_search(SEARCH_CRITERIA)
end
if uids.length > 0
uid_fetch_block(dest, uids, ['ENVELOPE']) do |data|
if data.attr['ENVELOPE'].message_id != nil then
dest_info[data.attr['ENVELOPE'].message_id] = true
else
msg = dest.uid_fetch(data.attr['UID'], ['RFC822', 'FLAGS',
'INTERNALDATE']).first
dest_info[Digest::MD5.hexdigest(msg.attr['RFC822'])] = true
end
end
end
dd "Found #{uids.length} messages"
# Loop through all messages in the source folder.
if SEARCH_CRITERIA.length == 0 then
uids = source.uid_search(['ALL'])
else
uids = source.uid_search(SEARCH_CRITERIA)
end
ds "Found #{uids.length} messages"
if uids.length > 0
#### (LOOP) START MESSAGE TRANSFFER ####
uid_fetch_block(source, uids, ['ENVELOPE']) do |data|
if data.attr['ENVELOPE'].message_id != nil then
mid = data.attr['ENVELOPE'].message_id
else
tmp_msg = source.uid_fetch(data.attr['UID'], ['RFC822', 'FLAGS',
'INTERNALDATE']).first
mid = Digest::MD5.hexdigest(tmp_msg.attr['RFC822'])
end
# If this message is already in the destination folder, skip it.
if dest_info[mid]
@existing += 1
next
end
# Download the full message body from the source folder.
ds "[#{source_folder}]Downloading message #{mid}..."
tries = 0
begin
tries += 1
msg = source.uid_fetch(data.attr['UID'], ['RFC822', 'FLAGS',
'INTERNALDATE']).first
rescue Net::IMAP::Error => ex
if tries < 10
dd "Error: #{ex.message}. Retrying..."
sleep 1 * tries
retry
else
@failures += 1
dd "Error: #{ex.message}. Tried and failed #{tries} times; giving up on this message."
end
end
# Append the message to the destination folder, preserving flags and
# internal timestamp.
dd "[#{dest_folder}]Storing message #{mid}..."
tries = 0
begin
tries += 1
pp msg.attr['FLAGS']
store_flags = msg.attr['FLAGS'] & ACCEPTED_FLAGS
pp store_flags
dest.append(dest_folder, msg.attr['RFC822'], store_flags,
msg.attr['INTERNALDATE'])
@synced += 1
rescue Net::IMAP::Error => ex
if tries < 10
dd "Error: #{ex.message}. Retrying..."
sleep 1 * tries
retry
else
@failures += 1
dd "Error: #{ex.message}. Tried and failed #{tries} times; giving up on this message."
end
end
end
#### END MESSAGE TRANSFFER ####
end
source.close
dest.close
end
src_thread.exit
dst_thread.exit
puts "Finished. Message counts: #{@existing} untouched, #{@synced} transferred, #{@failures} failures."