imap-backup 5.2.0 → 6.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -2
- data/docs/development.md +10 -4
- data/lib/cli_coverage.rb +1 -1
- data/lib/imap/backup/account/connection.rb +7 -11
- data/lib/imap/backup/account.rb +31 -11
- data/lib/imap/backup/cli/folders.rb +3 -3
- data/lib/imap/backup/cli/migrate.rb +3 -3
- data/lib/imap/backup/cli/utils.rb +2 -2
- data/lib/imap/backup/configuration.rb +1 -11
- data/lib/imap/backup/downloader.rb +13 -9
- data/lib/imap/backup/serializer/directory.rb +37 -0
- data/lib/imap/backup/serializer/imap.rb +120 -0
- data/lib/imap/backup/serializer/mbox.rb +23 -94
- data/lib/imap/backup/serializer/mbox_enumerator.rb +2 -0
- data/lib/imap/backup/serializer.rb +180 -3
- data/lib/imap/backup/setup/account.rb +52 -29
- data/lib/imap/backup/setup/helpers.rb +1 -1
- data/lib/imap/backup/thunderbird/mailbox_exporter.rb +1 -1
- data/lib/imap/backup/version.rb +3 -3
- data/lib/imap/backup.rb +0 -1
- data/spec/features/backup_spec.rb +8 -16
- data/spec/features/support/aruba.rb +4 -3
- data/spec/unit/imap/backup/account/connection_spec.rb +36 -8
- data/spec/unit/imap/backup/account/folder_spec.rb +10 -0
- data/spec/unit/imap/backup/account_spec.rb +246 -0
- data/spec/unit/imap/backup/cli/accounts_spec.rb +12 -1
- data/spec/unit/imap/backup/cli/backup_spec.rb +19 -0
- data/spec/unit/imap/backup/cli/folders_spec.rb +39 -0
- data/spec/unit/imap/backup/cli/local_spec.rb +26 -7
- data/spec/unit/imap/backup/cli/migrate_spec.rb +80 -0
- data/spec/unit/imap/backup/cli/restore_spec.rb +67 -0
- data/spec/unit/imap/backup/cli/setup_spec.rb +17 -0
- data/spec/unit/imap/backup/cli/utils_spec.rb +68 -5
- data/spec/unit/imap/backup/cli_spec.rb +93 -0
- data/spec/unit/imap/backup/client/apple_mail_spec.rb +9 -0
- data/spec/unit/imap/backup/configuration_spec.rb +2 -2
- data/spec/unit/imap/backup/downloader_spec.rb +59 -7
- data/spec/unit/imap/backup/migrator_spec.rb +1 -1
- data/spec/unit/imap/backup/sanitizer_spec.rb +42 -0
- data/spec/unit/imap/backup/serializer/directory_spec.rb +37 -0
- data/spec/unit/imap/backup/serializer/imap_spec.rb +218 -0
- data/spec/unit/imap/backup/serializer/mbox_spec.rb +62 -183
- data/spec/unit/imap/backup/serializer_spec.rb +296 -0
- data/spec/unit/imap/backup/setup/account_spec.rb +120 -25
- data/spec/unit/imap/backup/setup/helpers_spec.rb +15 -0
- data/spec/unit/imap/backup/thunderbird/mailbox_exporter_spec.rb +116 -0
- data/spec/unit/imap/backup/uploader_spec.rb +1 -1
- data/spec/unit/retry_on_error_spec.rb +34 -0
- metadata +36 -7
- data/lib/imap/backup/serializer/mbox_store.rb +0 -217
- data/spec/unit/imap/backup/serializer/mbox_store_spec.rb +0 -329
| @@ -1,115 +1,44 @@ | |
| 1 | 
            -
            require "forwardable"
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require "imap/backup/serializer/mbox_store"
         | 
| 4 | 
            -
             | 
| 5 1 | 
             
            module Imap::Backup
         | 
| 6 2 | 
             
              class Serializer::Mbox
         | 
| 7 | 
            -
                 | 
| 8 | 
            -
                def_delegators :store, :mbox_pathname
         | 
| 3 | 
            +
                attr_reader :folder_path
         | 
| 9 4 |  | 
| 10 | 
            -
                 | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
                def initialize(path, folder)
         | 
| 14 | 
            -
                  @path = path
         | 
| 15 | 
            -
                  @folder = folder
         | 
| 5 | 
            +
                def initialize(folder_path)
         | 
| 6 | 
            +
                  @folder_path = folder_path
         | 
| 16 7 | 
             
                end
         | 
| 17 8 |  | 
| 18 | 
            -
                def  | 
| 19 | 
            -
                   | 
| 20 | 
            -
             | 
| 21 | 
            -
                    store.uid_validity = value
         | 
| 22 | 
            -
                    nil
         | 
| 23 | 
            -
                  when store.uid_validity == value
         | 
| 24 | 
            -
                    # NOOP
         | 
| 25 | 
            -
                    nil
         | 
| 26 | 
            -
                  else
         | 
| 27 | 
            -
                    apply_new_uid_validity value
         | 
| 9 | 
            +
                def append(message)
         | 
| 10 | 
            +
                  File.open(pathname, "ab") do |file|
         | 
| 11 | 
            +
                    file.write message
         | 
| 28 12 | 
             
                  end
         | 
| 29 13 | 
             
                end
         | 
| 30 14 |  | 
| 31 | 
            -
                def  | 
| 32 | 
            -
                   | 
| 15 | 
            +
                def exist?
         | 
| 16 | 
            +
                  File.exist?(pathname)
         | 
| 33 17 | 
             
                end
         | 
| 34 18 |  | 
| 35 | 
            -
                def  | 
| 36 | 
            -
                   | 
| 37 | 
            -
                end
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                def load(uid)
         | 
| 40 | 
            -
                  store.load(uid)
         | 
| 41 | 
            -
                end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                def each_message(uids)
         | 
| 44 | 
            -
                  store.each_message(uids)
         | 
| 45 | 
            -
                end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                def save(uid, message)
         | 
| 48 | 
            -
                  store.add(uid, message)
         | 
| 49 | 
            -
                end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                def rename(new_name)
         | 
| 52 | 
            -
                  @folder = new_name
         | 
| 53 | 
            -
                  store.rename new_name
         | 
| 54 | 
            -
                end
         | 
| 19 | 
            +
                def length
         | 
| 20 | 
            +
                  return nil if !exist?
         | 
| 55 21 |  | 
| 56 | 
            -
             | 
| 57 | 
            -
                  store.update_uid old, new
         | 
| 22 | 
            +
                  File.stat(pathname).size
         | 
| 58 23 | 
             
                end
         | 
| 59 24 |  | 
| 60 | 
            -
                 | 
| 61 | 
            -
             | 
| 62 | 
            -
                def store
         | 
| 63 | 
            -
                  @store ||=
         | 
| 64 | 
            -
                    begin
         | 
| 65 | 
            -
                      create_containing_directory
         | 
| 66 | 
            -
                      Serializer::MboxStore.new(path, folder)
         | 
| 67 | 
            -
                    end
         | 
| 25 | 
            +
                def pathname
         | 
| 26 | 
            +
                  "#{folder_path}.mbox"
         | 
| 68 27 | 
             
                end
         | 
| 69 28 |  | 
| 70 | 
            -
                def  | 
| 71 | 
            -
                   | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
                     | 
| 75 | 
            -
             | 
| 76 | 
            -
                     | 
| 77 | 
            -
                    break if !test_store.exist?
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                    digit += 1
         | 
| 29 | 
            +
                def rename(new_path)
         | 
| 30 | 
            +
                  if exist?
         | 
| 31 | 
            +
                    old_pathname = pathname
         | 
| 32 | 
            +
                    @folder_path = new_path
         | 
| 33 | 
            +
                    File.rename(old_pathname, pathname)
         | 
| 34 | 
            +
                  else
         | 
| 35 | 
            +
                    @folder_path = new_path
         | 
| 80 36 | 
             
                  end
         | 
| 81 | 
            -
                  rename_store new_name, value
         | 
| 82 | 
            -
                end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                def rename_store(new_name, value)
         | 
| 85 | 
            -
                  store.rename new_name
         | 
| 86 | 
            -
                  @store = nil
         | 
| 87 | 
            -
                  store.uid_validity = value
         | 
| 88 | 
            -
                  new_name
         | 
| 89 | 
            -
                end
         | 
| 90 | 
            -
             | 
| 91 | 
            -
                def relative_path
         | 
| 92 | 
            -
                  File.dirname(folder)
         | 
| 93 37 | 
             
                end
         | 
| 94 38 |  | 
| 95 | 
            -
                def  | 
| 96 | 
            -
                  File. | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
                def full_path
         | 
| 100 | 
            -
                  File.expand_path(containing_directory)
         | 
| 101 | 
            -
                end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                def create_containing_directory
         | 
| 104 | 
            -
                  if !File.directory?(full_path)
         | 
| 105 | 
            -
                    Utils.make_folder(
         | 
| 106 | 
            -
                      path, relative_path, Serializer::DIRECTORY_PERMISSIONS
         | 
| 107 | 
            -
                    )
         | 
| 108 | 
            -
                  end
         | 
| 109 | 
            -
             | 
| 110 | 
            -
                  if Utils.mode(full_path) !=
         | 
| 111 | 
            -
                     Serializer::DIRECTORY_PERMISSIONS
         | 
| 112 | 
            -
                    FileUtils.chmod Serializer::DIRECTORY_PERMISSIONS, full_path
         | 
| 39 | 
            +
                def rewind(length)
         | 
| 40 | 
            +
                  File.open(pathname, File::RDWR | File::CREAT, 0o644) do |f|
         | 
| 41 | 
            +
                    f.truncate(length)
         | 
| 113 42 | 
             
                  end
         | 
| 114 43 | 
             
                end
         | 
| 115 44 | 
             
              end
         | 
| @@ -1,6 +1,183 @@ | |
| 1 | 
            +
            require "forwardable"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "email/mboxrd/message"
         | 
| 4 | 
            +
            require "imap/backup/serializer/imap"
         | 
| 5 | 
            +
            require "imap/backup/serializer/mbox"
         | 
| 6 | 
            +
            require "imap/backup/serializer/mbox_enumerator"
         | 
| 7 | 
            +
             | 
| 1 8 | 
             
            module Imap::Backup
         | 
| 2 | 
            -
               | 
| 3 | 
            -
                 | 
| 4 | 
            -
             | 
| 9 | 
            +
              class Serializer
         | 
| 10 | 
            +
                extend Forwardable
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def_delegator :mbox, :pathname, :mbox_pathname
         | 
| 13 | 
            +
                def_delegators :imap, :uid_validity, :uids, :update_uid
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                attr_reader :folder
         | 
| 16 | 
            +
                attr_reader :path
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def initialize(path, folder)
         | 
| 19 | 
            +
                  @path = path
         | 
| 20 | 
            +
                  @folder = folder
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def apply_uid_validity(value)
         | 
| 24 | 
            +
                  case
         | 
| 25 | 
            +
                  when uid_validity.nil?
         | 
| 26 | 
            +
                    imap.uid_validity = value
         | 
| 27 | 
            +
                    nil
         | 
| 28 | 
            +
                  when uid_validity == value
         | 
| 29 | 
            +
                    # NOOP
         | 
| 30 | 
            +
                    nil
         | 
| 31 | 
            +
                  else
         | 
| 32 | 
            +
                    apply_new_uid_validity value
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def force_uid_validity(value)
         | 
| 37 | 
            +
                  imap.uid_validity = value
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def append(uid, message)
         | 
| 41 | 
            +
                  raise "Can't add messages without uid_validity" if !imap.uid_validity
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  uid = uid.to_i
         | 
| 44 | 
            +
                  if imap.include?(uid)
         | 
| 45 | 
            +
                    Logger.logger.debug(
         | 
| 46 | 
            +
                      "[#{folder}] message #{uid} already downloaded - skipping"
         | 
| 47 | 
            +
                    )
         | 
| 48 | 
            +
                    return
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  do_append uid, message
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def load(uid_maybe_string)
         | 
| 55 | 
            +
                  uid = uid_maybe_string.to_i
         | 
| 56 | 
            +
                  message_index = imap.index(uid)
         | 
| 57 | 
            +
                  return nil if message_index.nil?
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  load_nth(message_index)
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def load_nth(index)
         | 
| 63 | 
            +
                  enumerator = Serializer::MboxEnumerator.new(mbox.pathname)
         | 
| 64 | 
            +
                  enumerator.each.with_index do |raw, i|
         | 
| 65 | 
            +
                    next if i != index
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    return Email::Mboxrd::Message.from_serialized(raw)
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                  nil
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def each_message(required_uids)
         | 
| 73 | 
            +
                  return enum_for(:each_message, required_uids) if !block_given?
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  indexes = required_uids.each.with_object({}) do |uid_maybe_string, acc|
         | 
| 76 | 
            +
                    uid = uid_maybe_string.to_i
         | 
| 77 | 
            +
                    index = imap.index(uid)
         | 
| 78 | 
            +
                    acc[index] = uid if index
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                  enumerator = Serializer::MboxEnumerator.new(mbox.pathname)
         | 
| 81 | 
            +
                  enumerator.each.with_index do |raw, i|
         | 
| 82 | 
            +
                    uid = indexes[i]
         | 
| 83 | 
            +
                    next if !uid
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    yield uid, Email::Mboxrd::Message.from_serialized(raw)
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def rename(new_name)
         | 
| 90 | 
            +
                  # Initialize so we get memoized instances with the correct folder_path
         | 
| 91 | 
            +
                  mbox
         | 
| 92 | 
            +
                  imap
         | 
| 93 | 
            +
                  @folder = new_name
         | 
| 94 | 
            +
                  ensure_containing_directory
         | 
| 95 | 
            +
                  mbox.rename folder_path
         | 
| 96 | 
            +
                  imap.rename folder_path
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                private
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def do_append(uid, message)
         | 
| 102 | 
            +
                  mboxrd_message = Email::Mboxrd::Message.new(message)
         | 
| 103 | 
            +
                  initial = mbox.length || 0
         | 
| 104 | 
            +
                  mbox_appended = false
         | 
| 105 | 
            +
                  begin
         | 
| 106 | 
            +
                    mbox.append mboxrd_message.to_serialized
         | 
| 107 | 
            +
                    mbox_appended = true
         | 
| 108 | 
            +
                    imap.append uid
         | 
| 109 | 
            +
                  rescue StandardError => e
         | 
| 110 | 
            +
                    mbox.rewind(initial) if mbox_appended
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    message = <<-ERROR.gsub(/^\s*/m, "")
         | 
| 113 | 
            +
                      [#{folder}] failed to append message #{uid}:
         | 
| 114 | 
            +
                      #{message}. #{e}:
         | 
| 115 | 
            +
                      #{e.backtrace.join("\n")}"
         | 
| 116 | 
            +
                    ERROR
         | 
| 117 | 
            +
                    Logger.logger.warn message
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def mbox
         | 
| 122 | 
            +
                  @mbox ||=
         | 
| 123 | 
            +
                    begin
         | 
| 124 | 
            +
                      ensure_containing_directory
         | 
| 125 | 
            +
                      Serializer::Mbox.new(folder_path)
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def imap
         | 
| 130 | 
            +
                  @imap ||=
         | 
| 131 | 
            +
                    begin
         | 
| 132 | 
            +
                      ensure_containing_directory
         | 
| 133 | 
            +
                      Serializer::Imap.new(folder_path)
         | 
| 134 | 
            +
                    end
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def folder_path
         | 
| 138 | 
            +
                  folder_path_for(path, folder)
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def folder_path_for(path, folder)
         | 
| 142 | 
            +
                  relative = File.join(path, folder)
         | 
| 143 | 
            +
                  File.expand_path(relative)
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                def ensure_containing_directory
         | 
| 147 | 
            +
                  relative = File.dirname(folder)
         | 
| 148 | 
            +
                  directory = Serializer::Directory.new(path, relative)
         | 
| 149 | 
            +
                  directory.ensure_exists
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                def apply_new_uid_validity(value)
         | 
| 153 | 
            +
                  new_name = rename_existing_folder
         | 
| 154 | 
            +
                  # Clear memoization so we get empty data
         | 
| 155 | 
            +
                  @mbox = nil
         | 
| 156 | 
            +
                  @imap = nil
         | 
| 157 | 
            +
                  imap.uid_validity = value
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  new_name
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                def rename_existing_folder
         | 
| 163 | 
            +
                  digit = 0
         | 
| 164 | 
            +
                  new_name = nil
         | 
| 165 | 
            +
                  loop do
         | 
| 166 | 
            +
                    extra = digit.zero? ? "" : "-#{digit}"
         | 
| 167 | 
            +
                    new_name = "#{folder}-#{imap.uid_validity}#{extra}"
         | 
| 168 | 
            +
                    new_folder_path = folder_path_for(path, new_name)
         | 
| 169 | 
            +
                    test_mbox = Serializer::Mbox.new(new_folder_path)
         | 
| 170 | 
            +
                    test_imap = Serializer::Imap.new(new_folder_path)
         | 
| 171 | 
            +
                    break if !test_mbox.exist? && !test_imap.exist?
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    digit += 1
         | 
| 174 | 
            +
                  end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                  previous = folder
         | 
| 177 | 
            +
                  rename(new_name)
         | 
| 178 | 
            +
                  @folder = previous
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  new_name
         | 
| 181 | 
            +
                end
         | 
| 5 182 | 
             
              end
         | 
| 6 183 | 
             
            end
         | 
| @@ -26,6 +26,7 @@ module Imap::Backup | |
| 26 26 | 
             
                    modify_password menu
         | 
| 27 27 | 
             
                    modify_backup_path menu
         | 
| 28 28 | 
             
                    choose_folders menu
         | 
| 29 | 
            +
                    modify_multi_fetch_size menu
         | 
| 29 30 | 
             
                    modify_server menu
         | 
| 30 31 | 
             
                    modify_connection_options menu
         | 
| 31 32 | 
             
                    test_connection menu
         | 
| @@ -37,21 +38,30 @@ module Imap::Backup | |
| 37 38 |  | 
| 38 39 | 
             
                def header(menu)
         | 
| 39 40 | 
             
                  modified = account.modified? ? "*" : ""
         | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                     | 
| 41 | 
            +
             | 
| 42 | 
            +
                  if account.multi_fetch_size > 1
         | 
| 43 | 
            +
                    multi_fetch_size = "\nmulti-fetch #{account.multi_fetch_size}"
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  if account.connection_options
         | 
| 47 | 
            +
                    escaped =
         | 
| 48 | 
            +
                      JSON.generate(account.connection_options)
         | 
| 49 | 
            +
                    connection_options =
         | 
| 50 | 
            +
                      "\nconnection options  '#{escaped}'"
         | 
| 51 | 
            +
                    space = " " * 12
         | 
| 52 | 
            +
                  else
         | 
| 53 | 
            +
                    connection_options = nil
         | 
| 54 | 
            +
                    space = " " * 4
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 47 57 | 
             
                  menu.header = <<~HEADER.chomp
         | 
| 48 58 | 
             
                    #{helpers.title_prefix} Account#{modified}
         | 
| 49 59 |  | 
| 50 | 
            -
                    email | 
| 51 | 
            -
                    password | 
| 52 | 
            -
                    path | 
| 53 | 
            -
                    folders | 
| 54 | 
            -
                    server | 
| 60 | 
            +
                    email   #{space}#{account.username}
         | 
| 61 | 
            +
                    password#{space}#{masked_password}
         | 
| 62 | 
            +
                    path    #{space}#{account.local_path}
         | 
| 63 | 
            +
                    folders #{space}#{folders.map { |f| f[:name] }.join(', ')}#{multi_fetch_size}
         | 
| 64 | 
            +
                    server  #{space}#{account.server}#{connection_options}
         | 
| 55 65 |  | 
| 56 66 | 
             
                    Choose an action
         | 
| 57 67 | 
             
                  HEADER
         | 
| @@ -70,12 +80,10 @@ module Imap::Backup | |
| 70 80 | 
             
                      )
         | 
| 71 81 | 
             
                    else
         | 
| 72 82 | 
             
                      account.username = username
         | 
| 73 | 
            -
                      # rubocop:disable Style/IfUnlessModifier
         | 
| 74 83 | 
             
                      default = default_server(username)
         | 
| 75 84 | 
             
                      if default && (account.server.nil? || (account.server == ""))
         | 
| 76 85 | 
             
                        account.server = default
         | 
| 77 86 | 
             
                      end
         | 
| 78 | 
            -
                      # rubocop:enable Style/IfUnlessModifier
         | 
| 79 87 | 
             
                    end
         | 
| 80 88 | 
             
                  end
         | 
| 81 89 | 
             
                end
         | 
| @@ -88,20 +96,6 @@ module Imap::Backup | |
| 88 96 | 
             
                  end
         | 
| 89 97 | 
             
                end
         | 
| 90 98 |  | 
| 91 | 
            -
                def modify_server(menu)
         | 
| 92 | 
            -
                  menu.choice("modify server") do
         | 
| 93 | 
            -
                    server = highline.ask("server: ")
         | 
| 94 | 
            -
                    account.server = server if !server.nil?
         | 
| 95 | 
            -
                  end
         | 
| 96 | 
            -
                end
         | 
| 97 | 
            -
             | 
| 98 | 
            -
                def modify_connection_options(menu)
         | 
| 99 | 
            -
                  menu.choice("modify connection options") do
         | 
| 100 | 
            -
                    connection_options = highline.ask("connections options (as JSON): ")
         | 
| 101 | 
            -
                    account.connection_options = connection_options if !connection_options.nil?
         | 
| 102 | 
            -
                  end
         | 
| 103 | 
            -
                end
         | 
| 104 | 
            -
             | 
| 105 99 | 
             
                def path_modification_validator(path)
         | 
| 106 100 | 
             
                  same = config.accounts.find do |a|
         | 
| 107 101 | 
             
                    a.username != account.username && a.local_path == path
         | 
| @@ -130,6 +124,35 @@ module Imap::Backup | |
| 130 124 | 
             
                  end
         | 
| 131 125 | 
             
                end
         | 
| 132 126 |  | 
| 127 | 
            +
                def modify_multi_fetch_size(menu)
         | 
| 128 | 
            +
                  menu.choice("modify multi-fetch size (number of emails to fetch at a time)") do
         | 
| 129 | 
            +
                    size = highline.ask("size: ")
         | 
| 130 | 
            +
                    int = size.to_i
         | 
| 131 | 
            +
                    account.multi_fetch_size = int if int.positive?
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def modify_server(menu)
         | 
| 136 | 
            +
                  menu.choice("modify server") do
         | 
| 137 | 
            +
                    server = highline.ask("server: ")
         | 
| 138 | 
            +
                    account.server = server if !server.nil?
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def modify_connection_options(menu)
         | 
| 143 | 
            +
                  menu.choice("modify connection options") do
         | 
| 144 | 
            +
                    connection_options = highline.ask("connections options (as JSON): ")
         | 
| 145 | 
            +
                    if !connection_options.nil?
         | 
| 146 | 
            +
                      begin
         | 
| 147 | 
            +
                        account.connection_options = connection_options
         | 
| 148 | 
            +
                      rescue JSON::ParserError
         | 
| 149 | 
            +
                        Kernel.puts "Malformed JSON, please try again"
         | 
| 150 | 
            +
                        highline.ask "Press a key "
         | 
| 151 | 
            +
                      end
         | 
| 152 | 
            +
                    end
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 133 156 | 
             
                def test_connection(menu)
         | 
| 134 157 | 
             
                  menu.choice("test connection") do
         | 
| 135 158 | 
             
                    result = Setup::ConnectionTester.new(account).test
         | 
| @@ -141,7 +164,7 @@ module Imap::Backup | |
| 141 164 | 
             
                def delete_account(menu)
         | 
| 142 165 | 
             
                  menu.choice("delete") do
         | 
| 143 166 | 
             
                    if highline.agree("Are you sure? (y/n) ")
         | 
| 144 | 
            -
                      account.mark_for_deletion | 
| 167 | 
            +
                      account.mark_for_deletion
         | 
| 145 168 | 
             
                      throw :done
         | 
| 146 169 | 
             
                    end
         | 
| 147 170 | 
             
                  end
         | 
    
        data/lib/imap/backup/version.rb
    CHANGED
    
    
    
        data/lib/imap/backup.rb
    CHANGED
    
    | @@ -8,7 +8,6 @@ require "imap/backup/downloader" | |
| 8 8 | 
             
            require "imap/backup/logger"
         | 
| 9 9 | 
             
            require "imap/backup/uploader"
         | 
| 10 10 | 
             
            require "imap/backup/serializer"
         | 
| 11 | 
            -
            require "imap/backup/serializer/mbox"
         | 
| 12 11 | 
             
            require "imap/backup/setup"
         | 
| 13 12 | 
             
            require "imap/backup/setup/account"
         | 
| 14 13 | 
             
            require "imap/backup/setup/asker"
         | 
| @@ -70,10 +70,18 @@ RSpec.describe "backup", type: :aruba, docker: true do | |
| 70 70 | 
             
                    expect(mbox_content(renamed_folder)).to eq(message_as_mbox_entry(msg3))
         | 
| 71 71 | 
             
                  end
         | 
| 72 72 |  | 
| 73 | 
            +
                  it "renames the old metadata file" do
         | 
| 74 | 
            +
                    expect(imap_parsed(renamed_folder)).to be_a Hash
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 73 77 | 
             
                  it "downloads messages" do
         | 
| 74 78 | 
             
                    expect(mbox_content(folder)).to eq(messages_as_mbox)
         | 
| 75 79 | 
             
                  end
         | 
| 76 80 |  | 
| 81 | 
            +
                  it "creates a metadata file" do
         | 
| 82 | 
            +
                    expect(imap_parsed(folder)).to be_a Hash
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 77 85 | 
             
                  context "when a renamed local backup exists" do
         | 
| 78 86 | 
             
                    let!(:pre) do
         | 
| 79 87 | 
             
                      super()
         | 
| @@ -88,21 +96,5 @@ RSpec.describe "backup", type: :aruba, docker: true do | |
| 88 96 | 
             
                    end
         | 
| 89 97 | 
             
                  end
         | 
| 90 98 | 
             
                end
         | 
| 91 | 
            -
             | 
| 92 | 
            -
                context "when an unversioned .imap file is found" do
         | 
| 93 | 
            -
                  let!(:pre) do
         | 
| 94 | 
            -
                    create_directory local_backup_path
         | 
| 95 | 
            -
                    File.open(imap_path(folder), "w") { |f| f.write "old format imap" }
         | 
| 96 | 
            -
                    File.open(mbox_path(folder), "w") { |f| f.write "old format emails" }
         | 
| 97 | 
            -
                  end
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                  it "replaces the .imap file with a versioned JSON file" do
         | 
| 100 | 
            -
                    expect(imap_metadata[:uids]).to eq(folder_uids)
         | 
| 101 | 
            -
                  end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                  it "does the download" do
         | 
| 104 | 
            -
                    expect(mbox_content(folder)).to eq(messages_as_mbox)
         | 
| 105 | 
            -
                  end
         | 
| 106 | 
            -
                end
         | 
| 107 99 | 
             
              end
         | 
| 108 100 | 
             
            end
         | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            require "aruba/rspec"
         | 
| 2 2 |  | 
| 3 3 | 
             
            require_relative "backup_directory"
         | 
| 4 | 
            +
            require "imap/backup/serializer/mbox"
         | 
| 4 5 |  | 
| 5 6 | 
             
            Aruba.configure do |config|
         | 
| 6 7 | 
             
              config.home_directory = File.expand_path("./tmp/home")
         | 
| @@ -36,10 +37,10 @@ module StoreHelpers | |
| 36 37 | 
             
                account = config.accounts.find { |a| a.username == email }
         | 
| 37 38 | 
             
                raise "Account not found" if !account
         | 
| 38 39 | 
             
                FileUtils.mkdir_p account.local_path
         | 
| 39 | 
            -
                 | 
| 40 | 
            -
                 | 
| 40 | 
            +
                serializer = Imap::Backup::Serializer.new(account.local_path, folder)
         | 
| 41 | 
            +
                serializer.force_uid_validity("42") if !serializer.uid_validity
         | 
| 41 42 | 
             
                serialized = to_serialized(from: from, subject: subject, body: body)
         | 
| 42 | 
            -
                 | 
| 43 | 
            +
                serializer.append uid, serialized
         | 
| 43 44 | 
             
              end
         | 
| 44 45 |  | 
| 45 46 | 
             
              def to_serialized(from:, subject:, body:)
         | 
| @@ -24,21 +24,24 @@ describe Imap::Backup::Account::Connection do | |
| 24 24 | 
             
              let(:account) do
         | 
| 25 25 | 
             
                instance_double(
         | 
| 26 26 | 
             
                  Imap::Backup::Account,
         | 
| 27 | 
            -
                  username:  | 
| 27 | 
            +
                  username: username,
         | 
| 28 28 | 
             
                  password: PASSWORD,
         | 
| 29 29 | 
             
                  local_path: LOCAL_PATH,
         | 
| 30 30 | 
             
                  folders: config_folders,
         | 
| 31 | 
            +
                  multi_fetch_size: multi_fetch_size,
         | 
| 31 32 | 
             
                  server: server,
         | 
| 32 33 | 
             
                  connection_options: nil
         | 
| 33 34 | 
             
                )
         | 
| 34 35 | 
             
              end
         | 
| 36 | 
            +
              let(:username) { USERNAME }
         | 
| 35 37 | 
             
              let(:config_folders) { [FOLDER_CONFIG] }
         | 
| 38 | 
            +
              let(:multi_fetch_size) { 1 }
         | 
| 36 39 | 
             
              let(:root_info) do
         | 
| 37 40 | 
             
                instance_double(Net::IMAP::MailboxList, name: ROOT_NAME)
         | 
| 38 41 | 
             
              end
         | 
| 39 42 | 
             
              let(:serializer) do
         | 
| 40 43 | 
             
                instance_double(
         | 
| 41 | 
            -
                  Imap::Backup::Serializer | 
| 44 | 
            +
                  Imap::Backup::Serializer,
         | 
| 42 45 | 
             
                  folder: serialized_folder,
         | 
| 43 46 | 
             
                  force_uid_validity: nil,
         | 
| 44 47 | 
             
                  apply_uid_validity: new_uid_validity,
         | 
| @@ -87,6 +90,23 @@ describe Imap::Backup::Account::Connection do | |
| 87 90 | 
             
                  end
         | 
| 88 91 | 
             
                end
         | 
| 89 92 |  | 
| 93 | 
            +
                context "when the provider is Apple" do
         | 
| 94 | 
            +
                  let(:username) { "user@mac.com" }
         | 
| 95 | 
            +
                  let(:apple_client) do
         | 
| 96 | 
            +
                    instance_double(
         | 
| 97 | 
            +
                      Imap::Backup::Client::AppleMail, login: nil
         | 
| 98 | 
            +
                    )
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  before do
         | 
| 102 | 
            +
                    allow(Imap::Backup::Client::AppleMail).to receive(:new) { apple_client }
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  it "returns the Apple client" do
         | 
| 106 | 
            +
                    expect(result).to eq(apple_client)
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
             | 
| 90 110 | 
             
                context "when run" do
         | 
| 91 111 | 
             
                  before { subject.client }
         | 
| 92 112 |  | 
| @@ -116,7 +136,7 @@ describe Imap::Backup::Account::Connection do | |
| 116 136 |  | 
| 117 137 | 
             
                before do
         | 
| 118 138 | 
             
                  allow(Imap::Backup::Account::Folder).to receive(:new) { folder }
         | 
| 119 | 
            -
                  allow(Imap::Backup::Serializer | 
| 139 | 
            +
                  allow(Imap::Backup::Serializer).to receive(:new) { serializer }
         | 
| 120 140 | 
             
                end
         | 
| 121 141 |  | 
| 122 142 | 
             
                it "creates the path" do
         | 
| @@ -150,16 +170,24 @@ describe Imap::Backup::Account::Connection do | |
| 150 170 | 
             
                let(:exists) { true }
         | 
| 151 171 | 
             
                let(:uid_validity) { 123 }
         | 
| 152 172 | 
             
                let(:downloader) { instance_double(Imap::Backup::Downloader, run: nil) }
         | 
| 173 | 
            +
                let(:multi_fetch_size) { 10 }
         | 
| 153 174 |  | 
| 154 175 | 
             
                before do
         | 
| 155 176 | 
             
                  allow(Imap::Backup::Downloader).
         | 
| 156 177 | 
             
                    to receive(:new).with(folder, serializer, anything) { downloader }
         | 
| 157 178 | 
             
                  allow(Imap::Backup::Account::Folder).to receive(:new).
         | 
| 158 179 | 
             
                    with(subject, BACKUP_FOLDER) { folder }
         | 
| 159 | 
            -
                  allow(Imap::Backup::Serializer | 
| 180 | 
            +
                  allow(Imap::Backup::Serializer).to receive(:new).
         | 
| 160 181 | 
             
                    with(LOCAL_PATH, IMAP_FOLDER) { serializer }
         | 
| 161 182 | 
             
                end
         | 
| 162 183 |  | 
| 184 | 
            +
                it "passes the multi_fetch_size" do
         | 
| 185 | 
            +
                  subject.run_backup
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  expect(Imap::Backup::Downloader).to have_received(:new).
         | 
| 188 | 
            +
                    with(anything, anything, {multi_fetch_size: 10})
         | 
| 189 | 
            +
                end
         | 
| 190 | 
            +
             | 
| 163 191 | 
             
                context "with supplied config_folders" do
         | 
| 164 192 | 
             
                  it "runs the downloader" do
         | 
| 165 193 | 
             
                    expect(downloader).to receive(:run)
         | 
| @@ -184,7 +212,7 @@ describe Imap::Backup::Account::Connection do | |
| 184 212 | 
             
                  before do
         | 
| 185 213 | 
             
                    allow(Imap::Backup::Account::Folder).to receive(:new).
         | 
| 186 214 | 
             
                      with(subject, ROOT_NAME) { folder }
         | 
| 187 | 
            -
                    allow(Imap::Backup::Serializer | 
| 215 | 
            +
                    allow(Imap::Backup::Serializer).to receive(:new).
         | 
| 188 216 | 
             
                      with(LOCAL_PATH, ROOT_NAME) { serializer }
         | 
| 189 217 | 
             
                  end
         | 
| 190 218 |  | 
| @@ -273,18 +301,18 @@ describe Imap::Backup::Account::Connection do | |
| 273 301 | 
             
                end
         | 
| 274 302 | 
             
                let(:updated_serializer) do
         | 
| 275 303 | 
             
                  instance_double(
         | 
| 276 | 
            -
                    Imap::Backup::Serializer | 
| 304 | 
            +
                    Imap::Backup::Serializer, force_uid_validity: nil
         | 
| 277 305 | 
             
                  )
         | 
| 278 306 | 
             
                end
         | 
| 279 307 |  | 
| 280 308 | 
             
                before do
         | 
| 281 309 | 
             
                  allow(Imap::Backup::Account::Folder).to receive(:new).
         | 
| 282 310 | 
             
                    with(subject, FOLDER_NAME) { folder }
         | 
| 283 | 
            -
                  allow(Imap::Backup::Serializer | 
| 311 | 
            +
                  allow(Imap::Backup::Serializer).to receive(:new).
         | 
| 284 312 | 
             
                    with(anything, FOLDER_NAME) { serializer }
         | 
| 285 313 | 
             
                  allow(Imap::Backup::Account::Folder).to receive(:new).
         | 
| 286 314 | 
             
                    with(subject, "new name") { updated_folder }
         | 
| 287 | 
            -
                  allow(Imap::Backup::Serializer | 
| 315 | 
            +
                  allow(Imap::Backup::Serializer).to receive(:new).
         | 
| 288 316 | 
             
                    with(anything, "new name") { updated_serializer }
         | 
| 289 317 | 
             
                  allow(Imap::Backup::Uploader).to receive(:new).
         | 
| 290 318 | 
             
                    with(folder, serializer) { uploader }
         | 
| @@ -64,6 +64,16 @@ describe Imap::Backup::Account::Folder do | |
| 64 64 | 
             
                    expect(subject.uids).to eq([])
         | 
| 65 65 | 
             
                  end
         | 
| 66 66 | 
             
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                context "when the UID search fails" do
         | 
| 69 | 
            +
                  before do
         | 
| 70 | 
            +
                    allow(client).to receive(:uid_search).and_raise(NoMethodError)
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  it "returns an empty array" do
         | 
| 74 | 
            +
                    expect(subject.uids).to eq([])
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 67 77 | 
             
              end
         | 
| 68 78 |  | 
| 69 79 | 
             
              describe "#fetch_multi" do
         |