Hello,

It is my first post on this forum so I will get to business straight forward.
I needed to migrate a lot of mailboxes from vpopmail and courier imap to Zimbra. I have tried to work with Imapsync and tried to find something better but after a long time I have found one ruby script that looked promising. Since the original script was not all I wanted it to be I have modified it to the point where it started to work for me. I have migrated with no issues mailboxes with more the 130,000 emails in them on the very slow machines. Here I post the actual script that I have modified to my liking, hope it will save some time to many people out there. It is very fast and very low memory consumption script. Enjoy.

Since the original script is under GPL license, I am forced to follow the trend.
If you find some bugs or discrepancies feel free to let me know at ian <at> askyan {dor} com.
Good luck.

Disclaimer: It is a fist time for me to touch "ruby" so I might not be doing some things the most efficient way.
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."