pEpForiOS/Models/CdMessage+Pantomime.swift
author buff <andreas@pep-project.org>
Mon, 07 Jan 2019 14:06:46 +0100
branchIOS-647
changeset 7440 790b3f124563
parent 6532 5c31d20b5660
child 7442 b1a815dc7290
permissions -rw-r--r--
IOS-647 static is implicitly final
dirk@1116
     1
//
dirk@1116
     2
//  CdMessage+Pantomime.swift
dirk@1116
     3
//  pEpForiOS
dirk@1116
     4
//
dirk@1116
     5
//  Created by Dirk Zimmermann on 23/11/16.
dirk@1116
     6
//  Copyright © 2016 p≡p Security S.A. All rights reserved.
dirk@1116
     7
//
dirk@1116
     8
dirk@1116
     9
import MessageModel
dirk@1116
    10
andreas@1822
    11
public typealias ImapStoreCommand = (command: String, pantomimeDict:[AnyHashable: Any])
andreas@1822
    12
andreas@2905
    13
public enum UpdateFlagsMode: String {
andreas@2905
    14
    case add = "+"
andreas@2905
    15
    case remove = "-"
andreas@2905
    16
}
andreas@1853
    17
dirk@1116
    18
extension CdMessage {
dirk@1116
    19
    /**
dirk@1116
    20
     - Returns: A `CWFlags object` for the given `NSNumber`
dirk@1116
    21
     */
andreas@6532
    22
    static public func pantomimeFlagsFromNumber(_ flags: Int16) -> CWFlags {
andreas@5906
    23
        if let fl = PantomimeFlag(rawValue: UInt(flags)) {
andreas@5906
    24
            return CWFlags(flags: fl)
dirk@1116
    25
        }
dirk@1116
    26
        Log.error(component:
dirk@1116
    27
            "Message", errorString:
dirk@1116
    28
            "Could not convert \(flags) to PantomimeFlag")
andreas@5906
    29
        return CWFlags()
dirk@1116
    30
    }
dirk@1116
    31
dirk@1116
    32
    /**
dirk@1116
    33
     - Returns: The current flags as String, like "\Deleted \Answered"
dirk@1116
    34
     */
dirk@1116
    35
    static func flagsStringFromNumber(_ flags: Int16) -> String {
dirk@1116
    36
        return pantomimeFlagsFromNumber(flags).asString()
dirk@1116
    37
    }
andreas@1853
    38
dirk@1116
    39
    /**
dirk@1116
    40
     - Returns: `flags` as `CWFlags`
dirk@1116
    41
     */
dirk@1116
    42
    public func pantomimeFlags() -> CWFlags {
dirk@1884
    43
        if let theFlags = imap?.localFlags {
dirk@1884
    44
            return theFlags.pantomimeFlags() ?? CWFlags()
dirk@1525
    45
        } else {
dirk@1525
    46
            return CWFlags()
dirk@1525
    47
        }
dirk@1116
    48
    }
dirk@1116
    49
dirk@1116
    50
    /**
dirk@1116
    51
     - Returns: `flagsFromServer` as `CWFlags`
dirk@1116
    52
     */
andreas@1853
    53
    public func pantomimeflagsFromServer() -> CWFlags {
dirk@1884
    54
        if let theFlags = imap?.serverFlags {
dirk@1884
    55
            return theFlags.pantomimeFlags() ?? CWFlags()
dirk@1884
    56
        } else {
dirk@1884
    57
            return CWFlags()
dirk@1884
    58
        }
dirk@1116
    59
    }
andreas@1853
    60
dirk@1116
    61
    /**
dirk@1116
    62
     - Returns: A `CWFlags object` for the given `Int16`
dirk@1116
    63
     */
andreas@7440
    64
    static public func pantomimeFlags(flagsInt16: Int16) -> CWFlags {
dirk@1116
    65
        if let fl = PantomimeFlag(rawValue: UInt(flagsInt16)) {
andreas@5906
    66
            return CWFlags(flags: fl)
dirk@1116
    67
        }
dirk@1116
    68
        Log.error(component:
dirk@1116
    69
            "Message", errorString:
dirk@1116
    70
            "Could not convert \(flagsInt16) to PantomimeFlag")
andreas@5906
    71
        return CWFlags()
dirk@1116
    72
    }
dirk@1116
    73
dirk@1116
    74
    /**
dirk@1116
    75
     - Returns: The current flags as String, like "\Deleted \Answered"
dirk@1116
    76
     */
dirk@1116
    77
    static func flagsString(flagsInt16: Int16) -> String {
dirk@1116
    78
        return pantomimeFlags(flagsInt16: flagsInt16).asString()
dirk@1116
    79
    }
dirk@1116
    80
andreas@1822
    81
    private func pantomimeInfoDict() -> [AnyHashable: Any] {
andreas@1822
    82
        // Construct a very minimal pantomime dummy for the info dictionary
andreas@1822
    83
        let pantomimeMail = CWIMAPMessage.init()
andreas@1822
    84
        pantomimeMail.setUID(UInt(uid))
andreas@1822
    85
        var dict: [AnyHashable: Any] = [PantomimeMessagesKey: NSArray.init(object: pantomimeMail)]
andreas@1822
    86
andreas@1822
    87
        guard let imap = imap else {
andreas@1822
    88
            Log.shared.errorAndCrash(component:"\(#function)[\(#line)]", errorString: "imap == nil")
andreas@1822
    89
            return [AnyHashable: Any]()
andreas@1822
    90
        }
andreas@1822
    91
dirk@1889
    92
        if let currentFlags = imap.localFlags?.rawFlagsAsShort() {
dirk@1884
    93
            dict[PantomimeFlagsKey] = CdMessage.pantomimeFlags(flagsInt16: currentFlags)
dirk@1884
    94
        }
andreas@1822
    95
andreas@1822
    96
        return dict
andreas@1822
    97
    }
andreas@1822
    98
andreas@2905
    99
    /// Creates a tuple consisting of an IMAP command string for syncing flags that have been
andreas@2905
   100
    /// modified by the client for this message, and a dictionary suitable for using pantomime
andreas@1853
   101
    /// for the actual execution.
andreas@1853
   102
    ///
andreas@2905
   103
    /// - note: Flags added and flags removed by the client use different commands.
andreas@1853
   104
    ///         Which to use can be chosen by the `mode` parameted.
andreas@1853
   105
    ///
andreas@1853
   106
    /// - seealso: [RFC4549](https://tools.ietf.org/html/rfc4549#section-4.2.3)
andreas@1853
   107
    ///
andreas@1853
   108
    /// - Parameter mode: mode to create command for
andreas@2905
   109
    /// - Returns: tuple consisting of an IMAP command string for syncing flags and a dictionary
andreas@1853
   110
    ///    suitable for using pantomime
andreas@1853
   111
    /// for the actual execution
andreas@1853
   112
    public func storeCommandForUpdateFlags(to mode: UpdateFlagsMode) -> ImapStoreCommand? {
andreas@1853
   113
        guard imap != nil else {
andreas@1810
   114
            return nil
andreas@1810
   115
        }
andreas@1810
   116
dirk@1889
   117
        let flags: ImapFlagsBits!
andreas@1853
   118
andreas@1853
   119
        switch mode {
andreas@1853
   120
        case .add:
andreas@1853
   121
            flags = flagsToAdd()
andreas@1853
   122
        case .remove:
andreas@1853
   123
            flags = flagsToRemove()
andreas@1853
   124
        }
andreas@1810
   125
andreas@1818
   126
        if flags.imapNoRelevantFlagSet() {
andreas@1810
   127
            return nil
andreas@1810
   128
        }
andreas@1810
   129
andreas@1853
   130
        let prefixFlagsSilent = mode.rawValue
andreas@1810
   131
andreas@1810
   132
        let flagsString = CdMessage.flagsString(flagsInt16: flags)
andreas@1853
   133
        let result = "UID STORE \(uid) \(prefixFlagsSilent)FLAGS.SILENT (\(flagsString))"
andreas@1810
   134
andreas@1822
   135
        let dict = pantomimeInfoDict()
andreas@1810
   136
andreas@1822
   137
        return ImapStoreCommand(command: result, pantomimeDict: dict)
andreas@1810
   138
    }
andreas@1810
   139
andreas@1810
   140
    /// Returns the flags that have to be added on server, represented in bits.
andreas@1810
   141
    /// A set bit (1) means it has to be added.
andreas@1810
   142
    ///
andreas@1810
   143
    /// - Returns: flags to add
andreas@1810
   144
    private func flagsToAdd() -> Int16 {
andreas@1810
   145
        let diff = flagsDiff()
andreas@1810
   146
andreas@1818
   147
        if !diff.imapAnyRelevantFlagSet() {
andreas@1853
   148
            return ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   149
        }
andreas@1810
   150
dirk@1884
   151
        guard let flagsCurrent = imap?.localFlags?.rawFlagsAsShort() else {
andreas@1853
   152
            return ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   153
        }
andreas@1810
   154
andreas@1853
   155
        var flagsToRemove = ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   156
andreas@1810
   157
        if diff.imapFlagBitIsSet(flagbit: .answered) && flagsCurrent.imapFlagBitIsSet(flagbit: .answered) {
andreas@1810
   158
            flagsToRemove += ImapFlagBit.answered.rawValue
andreas@1810
   159
        }
andreas@1810
   160
        if diff.imapFlagBitIsSet(flagbit: .deleted) && flagsCurrent.imapFlagBitIsSet(flagbit: .deleted) {
andreas@1810
   161
            flagsToRemove += ImapFlagBit.deleted.rawValue
andreas@1810
   162
        }
andreas@1824
   163
        if diff.imapFlagBitIsSet(flagbit: .draft) && flagsCurrent.imapFlagBitIsSet(flagbit: .draft) {
andreas@1824
   164
            flagsToRemove += ImapFlagBit.draft.rawValue
andreas@1824
   165
        }
andreas@1810
   166
        if diff.imapFlagBitIsSet(flagbit: .flagged) && flagsCurrent.imapFlagBitIsSet(flagbit: .flagged) {
andreas@1810
   167
            flagsToRemove += ImapFlagBit.flagged.rawValue
andreas@1810
   168
        }
andreas@1810
   169
        // The "Recent" flag is intentionally not handled, as it is modified by the server only.
andreas@1810
   170
        if diff.imapFlagBitIsSet(flagbit: .seen) && flagsCurrent.imapFlagBitIsSet(flagbit: .seen) {
andreas@1810
   171
            flagsToRemove += ImapFlagBit.seen.rawValue
andreas@1810
   172
        }
andreas@1810
   173
andreas@1810
   174
        return flagsToRemove
andreas@1810
   175
    }
andreas@1810
   176
andreas@1810
   177
    /// Returns the flags that have to be removed on server, represented in bits.
andreas@1810
   178
    /// A set bit (1) means it has to be removed.
andreas@1810
   179
    ///
andreas@1810
   180
    /// - Returns: flags to remove
andreas@1810
   181
    private func flagsToRemove() -> Int16 {
andreas@1810
   182
        let diff = flagsDiff()
andreas@1810
   183
andreas@1818
   184
        if !diff.imapAnyRelevantFlagSet() {
andreas@1853
   185
            return ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   186
        }
andreas@1810
   187
dirk@1884
   188
        guard let flagsCurrent = imap?.localFlags?.rawFlagsAsShort() else {
andreas@1853
   189
            return ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   190
        }
andreas@1810
   191
andreas@1853
   192
        var flagsToRemove = ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   193
andreas@1810
   194
        if diff.imapFlagBitIsSet(flagbit: .answered) && !flagsCurrent.imapFlagBitIsSet(flagbit: .answered) {
andreas@1810
   195
            flagsToRemove += ImapFlagBit.answered.rawValue
andreas@1810
   196
        }
andreas@1810
   197
        if diff.imapFlagBitIsSet(flagbit: .deleted) && !flagsCurrent.imapFlagBitIsSet(flagbit: .deleted) {
andreas@1810
   198
            flagsToRemove += ImapFlagBit.deleted.rawValue
andreas@1810
   199
        }
andreas@1824
   200
        if diff.imapFlagBitIsSet(flagbit: .draft) && !flagsCurrent.imapFlagBitIsSet(flagbit: .draft) {
andreas@1824
   201
            flagsToRemove += ImapFlagBit.draft.rawValue
andreas@1824
   202
        }
andreas@1810
   203
        if diff.imapFlagBitIsSet(flagbit: .flagged) && !flagsCurrent.imapFlagBitIsSet(flagbit: .flagged) {
andreas@1810
   204
            flagsToRemove += ImapFlagBit.flagged.rawValue
andreas@1810
   205
        }
andreas@1810
   206
        // The "Recent" flag is intentionally not handled, as it is modified by the server only.
andreas@1810
   207
        if diff.imapFlagBitIsSet(flagbit: .seen) && !flagsCurrent.imapFlagBitIsSet(flagbit: .seen) {
andreas@1810
   208
            flagsToRemove += ImapFlagBit.seen.rawValue
andreas@1810
   209
        }
andreas@1810
   210
andreas@1810
   211
        return flagsToRemove
andreas@1810
   212
    }
andreas@1810
   213
andreas@1810
   214
    /// Returns the flags that differ in between flagsCurrent and flagsFromServer, represented in bits.
andreas@1810
   215
    /// A set bit (1) means it differs.
andreas@1853
   216
    ///
andreas@1827
   217
    /// Find more details about the semantic of those bits in Int16+ImapFlagBits.swift
andreas@1810
   218
    ///
andreas@1810
   219
    /// - Returns: flags that differ
andreas@1810
   220
    private func flagsDiff() -> Int16 {
dirk@1884
   221
        guard let flagsCurrent = imap?.localFlags?.rawFlagsAsShort() else {
andreas@1853
   222
            return ImapFlagsBits.imapNoFlagsSet()
andreas@1810
   223
        }
andreas@1810
   224
andreas@1853
   225
        var flagsFromServer = ImapFlagsBits.imapNoFlagsSet()
dirk@1884
   226
        if let flags = imap?.serverFlags?.rawFlagsAsShort() {
andreas@1810
   227
            flagsFromServer = flags
andreas@1810
   228
        }
andreas@1810
   229
andreas@1818
   230
        return flagsCurrent ^ flagsFromServer
andreas@1810
   231
    }
andreas@1810
   232
dirk@1116
   233
    /**
dirk@1116
   234
     Convert the `Message` into an `CWIMAPMessage`, belonging to the given folder.
dirk@1116
   235
     - Note: This does not handle attachments and many other fields.
dirk@1116
   236
     *It's just for quickly interfacing with Pantomime.*
dirk@1116
   237
     */
dirk@1746
   238
    func pantomimeQuick(folder: CWIMAPFolder) -> CWIMAPMessage {
dirk@1116
   239
        let msg = CWIMAPMessage.init()
dirk@1116
   240
dirk@1491
   241
        if let date = sent {
dirk@1472
   242
            msg.setOriginationDate(date as Date)
dirk@1116
   243
        }
dirk@1116
   244
dirk@1116
   245
        if let sub = shortMessage {
dirk@1116
   246
            msg.setSubject(sub)
dirk@1116
   247
        }
dirk@1116
   248
dirk@1116
   249
        if let str = messageID {
dirk@1116
   250
            msg.setMessageID(str)
dirk@1116
   251
        }
dirk@1116
   252
dirk@1116
   253
        msg.setUID(UInt(uid))
dirk@1116
   254
dirk@1116
   255
        if let msn = imap?.messageNumber {
dirk@1116
   256
            msg.setMessageNumber(UInt(msn))
dirk@1116
   257
        }
dirk@1116
   258
dirk@1116
   259
        if let boundary = imap?.mimeBoundary {
dirk@1116
   260
            msg.setBoundary(boundary.data(using: String.Encoding.ascii))
dirk@1116
   261
        }
dirk@1116
   262
dirk@1116
   263
        if let contact = from {
dirk@1116
   264
            msg.setFrom(internetAddressFromContact(contact))
dirk@1116
   265
        }
dirk@1116
   266
dirk@1116
   267
        var recipients: [CWInternetAddress] = []
dirk@1116
   268
        collectContacts(cc, asPantomimeReceiverType: .ccRecipient,
dirk@1116
   269
                        intoTargetArray: &recipients)
dirk@1116
   270
        collectContacts(bcc, asPantomimeReceiverType: .bccRecipient,
dirk@1116
   271
                        intoTargetArray: &recipients)
dirk@1116
   272
        collectContacts(to, asPantomimeReceiverType: .toRecipient,
dirk@1116
   273
                        intoTargetArray: &recipients)
dirk@1116
   274
        msg.setRecipients(recipients)
dirk@1116
   275
dirk@1116
   276
        var refs: [String] = []
dirk@1116
   277
        if let theRefs = references {
dirk@1116
   278
            for ref in theRefs {
xavier@3000
   279
                if let tref = ref as? CdMessageReference, let refString: String = tref.reference {
dirk@1116
   280
                    refs.append(refString)
dirk@1116
   281
                }
dirk@1116
   282
            }
dirk@1116
   283
        }
dirk@1116
   284
        msg.setReferences(refs)
dirk@1116
   285
dirk@1116
   286
        if let ct = imap?.contentType {
dirk@1116
   287
            msg.setContentType(ct)
dirk@1116
   288
        }
dirk@1116
   289
dirk@1116
   290
        msg.setFolder(folder)
dirk@1116
   291
dirk@1116
   292
        // Avoid roundtrips to the server, just set the flags directly.
dirk@1884
   293
        if let flags = imap?.localFlags?.rawFlagsAsShort() {
dirk@1116
   294
            msg.flags().replace(with: CWFlags.init(int: Int(flags)))
dirk@1116
   295
        }
dirk@1116
   296
dirk@1116
   297
        return msg
dirk@1116
   298
    }
dirk@1116
   299
dirk@1177
   300
    public func pantomime() -> CWIMAPMessage {
dirk@1177
   301
        return PEPUtil.pantomime(cdMessage: self)
dirk@1177
   302
    }
dirk@1177
   303
dirk@1116
   304
    func internetAddressFromContact(_ contact: CdIdentity) -> CWInternetAddress {
dirk@2856
   305
        let address = contact.address ?? "" // CdIdentity.address is not optional in the DB
dirk@2856
   306
        return CWInternetAddress.init(personal: contact.userName, address: address)
dirk@1116
   307
    }
andreas@1853
   308
dirk@1116
   309
    func collectContacts(_ contacts: NSOrderedSet?,
dirk@1116
   310
                         asPantomimeReceiverType receiverType: PantomimeRecipientType,
dirk@1116
   311
                         intoTargetArray target: inout [CWInternetAddress]) {
dirk@1116
   312
        guard let cs = contacts else {
dirk@1116
   313
            return
dirk@1116
   314
        }
dirk@1116
   315
        for obj in cs {
dirk@1116
   316
            if let theContact = obj as? CdIdentity {
dirk@1116
   317
                let addr = internetAddressFromContact(theContact)
dirk@1116
   318
                addr.setType(receiverType)
dirk@1116
   319
                target.append(addr)
dirk@1116
   320
            }
dirk@1116
   321
        }
dirk@1116
   322
    }
dirk@1116
   323
dirk@1542
   324
    /**
dirk@2280
   325
     Stores server flags that have changed.
dirk@2280
   326
     * If the server flags have already been known, nothing is done.
dirk@2280
   327
     * Otherwise, the new server flags are stored.
andreas@2905
   328
     * If there were no local flag changes (in respect to the previous server flags version),
andreas@2905
   329
     the local flags will then be set to the same value.
andreas@2905
   330
     * If there were local changes, then the local flags will not change.
dirk@2182
   331
     - Returns: true if the local flags were updated.
dirk@1542
   332
     */
dirk@2182
   333
    public func updateFromServer(cwFlags: CWFlags) -> Bool {
dirk@1532
   334
        // Since we frequently sync the flags, don't modify anything
dirk@1532
   335
        // if the version from the server has already been known,
dirk@1532
   336
        // since this could overwrite changes just made by the user.
dirk@2182
   337
        if cwFlags.rawFlagsAsShort() == imap?.serverFlags?.rawFlagsAsShort() {
dirk@1542
   338
            return false
dirk@1532
   339
        }
dirk@1532
   340
dirk@2451
   341
        let theImap = imapFields()
dirk@1229
   342
dirk@2182
   343
        let haveLocalChanges =
dirk@2182
   344
            theImap.localFlags?.rawFlagsAsShort() != theImap.serverFlags?.rawFlagsAsShort()
dirk@1884
   345
dirk@2182
   346
        let serverFlags = theImap.serverFlags ?? CdImapFlags.create()
dirk@2182
   347
        theImap.serverFlags = serverFlags
dirk@2182
   348
dirk@2183
   349
        let localFlags = theImap.localFlags ?? CdImapFlags.create()
dirk@2183
   350
        theImap.localFlags = localFlags
dirk@2183
   351
dirk@2183
   352
        var changedLocalFlags = false
dirk@2183
   353
        if haveLocalChanges {
dirk@2183
   354
            changedLocalFlags =  mergeOnConflict(localFlags: localFlags, serverFlags: serverFlags,
dirk@2183
   355
                                                 newServerFlags: cwFlags)
dirk@2183
   356
        } else {
dirk@2183
   357
            localFlags.update(cwFlags: cwFlags)
dirk@2183
   358
            changedLocalFlags = true
dirk@2183
   359
        }
dirk@2183
   360
dirk@2182
   361
        serverFlags.update(cwFlags: cwFlags)
dirk@2182
   362
        if cwFlags.contain(.deleted) {
dirk@1558
   363
            Log.info(component: #function, content: "Message with flag deleted")
dirk@1558
   364
        }
dirk@1542
   365
dirk@2183
   366
        return changedLocalFlags
dirk@2183
   367
    }
dirk@2183
   368
dirk@2183
   369
    /**
dirk@2183
   370
     Tries to merge IMAP flags, basically taking into
dirk@2427
   371
     account which flags were changed locally if it makes any difference.
dirk@2183
   372
     */
dirk@2183
   373
    func mergeOnConflict(localFlags: CdImapFlags, serverFlags: CdImapFlags,
dirk@2183
   374
                         newServerFlags: CWFlags) -> Bool {
dirk@2183
   375
        localFlags.flagAnswered = localFlags.flagAnswered || serverFlags.flagAnswered ||
dirk@2183
   376
            newServerFlags.contain(.answered)
dirk@2183
   377
        localFlags.flagDraft = localFlags.flagDraft || serverFlags.flagDraft ||
dirk@2183
   378
            newServerFlags.contain(.draft)
dirk@2183
   379
        if localFlags.flagFlagged == serverFlags.flagFlagged {
dirk@2183
   380
            localFlags.flagFlagged = newServerFlags.contain(.flagged)
dirk@1887
   381
        }
dirk@2183
   382
        localFlags.flagRecent = newServerFlags.contain(.recent)
dirk@2183
   383
        if localFlags.flagSeen == serverFlags.flagSeen {
dirk@2183
   384
            localFlags.flagSeen = newServerFlags.contain(.seen)
dirk@2183
   385
        }
dirk@2183
   386
        localFlags.flagDeleted = localFlags.flagDeleted || serverFlags.flagDeleted ||
dirk@2183
   387
            newServerFlags.contain(.deleted)
dirk@2183
   388
        return localFlags.rawFlagsAsShort() != newServerFlags.rawFlagsAsShort()
dirk@1229
   389
    }
dirk@1229
   390
dirk@1116
   391
    /**
dirk@1210
   392
     Quickly inserts essential parts of a pantomime message into the store.
dirk@1210
   393
     Useful for networking, where inserts should be quick and the persistent store
dirk@1210
   394
     correct (especially in terms of UIDs, messageNumbers etc.)
dirk@1210
   395
     - Returns: The message just created or updated, or nil.
dirk@1116
   396
     */
dirk@1116
   397
    public static func quickInsertOrUpdate(
dirk@1116
   398
        pantomimeMessage message: CWIMAPMessage,
dirk@1524
   399
        account: CdAccount, messageUpdate: CWMessageUpdate) -> CdMessage? {
dirk@1116
   400
        guard let folderName = message.folder()?.name() else {
dirk@1116
   401
            return nil
dirk@1116
   402
        }
dirk@1116
   403
        guard let folder = account.folder(byName: folderName) else {
dirk@1116
   404
            return nil
dirk@1116
   405
        }
andreas@3382
   406
        
andreas@3382
   407
        var isUpdate = false
andreas@3159
   408
        let mail:CdMessage
andreas@3159
   409
        if let existing = existing(pantomimeMessage: message, inAccount: account) {
andreas@3159
   410
            mail = existing
andreas@3382
   411
            isUpdate = true
andreas@3159
   412
        } else {
andreas@3159
   413
            mail = CdMessage.create()
andreas@3159
   414
        }
andreas@3391
   415
andreas@3382
   416
        let oldMSN = mail.imapFields().messageNumber
andreas@3382
   417
        let newMSN = Int32(message.messageNumber())
andreas@3382
   418
        
andreas@3391
   419
        // Bail out quickly if there is only a MSN change
andreas@3391
   420
        if messageUpdate.isMsnOnly() {
andreas@3391
   421
            mail.imapFields().messageNumber = newMSN
andreas@3391
   422
            return mail
andreas@3391
   423
        }
andreas@3391
   424
        
andreas@3382
   425
        if mail.updateFromServer(cwFlags: message.flags()) {
andreas@3382
   426
            if mail.pEpRating != pEpRatingNone {
andreas@3382
   427
                mail.serialNumber = mail.serialNumber + 1
andreas@3382
   428
            }
andreas@3382
   429
        }
andreas@3382
   430
        // Bail out quickly if there is only a flag change needed
andreas@3382
   431
        if messageUpdate.isFlagsOnly() {
andreas@3382
   432
            guard isUpdate else {
andreas@3382
   433
                Log.shared.errorAndCrash(component: #function,
andreas@3382
   434
                                         errorString:
andreas@3382
   435
                    "If only flags did change, the message must have existed before. Thus it must be an update.")
andreas@3382
   436
                return nil
andreas@3382
   437
            }
andreas@3382
   438
            if oldMSN != newMSN {
andreas@3382
   439
                mail.imapFields().messageNumber = newMSN
andreas@3382
   440
            }
andreas@3391
   441
            informDelegate(messageUpdated: mail)
andreas@3382
   442
            return mail
andreas@3382
   443
        }
andreas@3382
   444
        
xavier@2938
   445
        if !moreMessagesThanRequested(mail: mail, messageUpdate: messageUpdate) {
xavier@2938
   446
            mail.parent = folder
xavier@2938
   447
            mail.bodyFetched = message.isInitialized()
dirk@3070
   448
            mail.sent = message.originationDate()
xavier@2938
   449
            mail.shortMessage = message.subject()
andreas@3382
   450
            
xavier@2938
   451
            mail.uuid = message.messageID()
xavier@2938
   452
            mail.uid = Int32(message.uid())
andreas@3382
   453
            
xavier@2938
   454
            let imap = mail.imapFields()
andreas@3382
   455
            
xavier@2938
   456
            imap.messageNumber = Int32(message.messageNumber())
xavier@2938
   457
            imap.mimeBoundary = (message.boundary() as NSData?)?.asciiString()
xavier@2938
   458
        }
andreas@3382
   459
        
andreas@3299
   460
        if isUpdate {
andreas@3391
   461
           informDelegate(messageUpdated: mail)
andreas@3159
   462
        }
andreas@3382
   463
        
dirk@1116
   464
        return mail
dirk@1116
   465
    }
andreas@3391
   466
    
andreas@3391
   467
    static private func informDelegate(messageUpdated cdMmessage:CdMessage) {
andreas@3391
   468
        guard let msg = cdMmessage.message(), let flags = msg.imapFlags else {
andreas@3391
   469
            return
andreas@3391
   470
        }
andreas@3391
   471
        if !flags.deleted {
andreas@3391
   472
            MessageModelConfig.messageFolderDelegate?.didUpdate(messageFolder: msg)
andreas@5667
   473
        } else {
dirk@5724
   474
            MessageModelConfig.messageFolderDelegate?.didDelete(
dirk@5724
   475
                messageFolder: msg,
dirk@5724
   476
                belongingToThread: Set())
andreas@5665
   477
        }
andreas@3391
   478
    }
dirk@1116
   479
dirk@1116
   480
    /**
dirk@1116
   481
     Converts a pantomime mail to a Message and stores it.
dirk@1116
   482
     Don't use this on the main thread as there is potentially a lot of processing involved
dirk@1116
   483
     (e.g., parsing of HTML and/or attachments).
dirk@1116
   484
     - Parameter message: The pantomime message to insert.
dirk@1210
   485
     - Parameter account: The account this email is supposed to be stored for.
dirk@1116
   486
     - Parameter forceParseAttachments: If true, this will parse the attachments even
dirk@1210
   487
     if the pantomime has not been initialized yet (useful for testing).
dirk@1116
   488
     - Returns: The newly created or updated Message
dirk@1116
   489
     */
andreas@3394
   490
    public static func insertOrUpdate( pantomimeMessage: CWIMAPMessage, account: CdAccount,
andreas@3394
   491
                                       messageUpdate: CWMessageUpdate,
andreas@3394
   492
                                       forceParseAttachments: Bool = false) -> CdMessage? {
andreas@3394
   493
        objc_sync_enter(self)
andreas@3394
   494
        defer { objc_sync_exit(self) }
andreas@3394
   495
        
dirk@1524
   496
        guard let mail = quickInsertOrUpdate(
dirk@1539
   497
            pantomimeMessage: pantomimeMessage, account: account, messageUpdate: messageUpdate)
dirk@1539
   498
            else {
dirk@1524
   499
                return nil
dirk@1524
   500
        }
dirk@1524
   501
andreas@3391
   502
        if messageUpdate.isFlagsOnly() || messageUpdate.isMsnOnly() {
andreas@3382
   503
            Record.saveAndWait()
dirk@1524
   504
            return mail
dirk@1116
   505
        }
dirk@1116
   506
xavier@2938
   507
        if moreMessagesThanRequested(mail: mail, messageUpdate: messageUpdate) {
dirk@1781
   508
            // This is a contradiction in itself, a new message that already existed.
dirk@1781
   509
            // Can happen with yahoo IMAP servers when they send more messages in
dirk@1781
   510
            // FETCH responses than requested.
dirk@1781
   511
            Log.warn(component: #function,
dirk@1781
   512
                     content: "ignoring rfc2822 update for already decrypted message")
dirk@1781
   513
            return mail
dirk@1781
   514
        }
dirk@1781
   515
dirk@1811
   516
        if let from = pantomimeMessage.from() {
dirk@1811
   517
            mail.from = cdIdentity(pantomimeAddress: from)
dirk@1116
   518
        }
dirk@1116
   519
dirk@1539
   520
        mail.bodyFetched = pantomimeMessage.isInitialized()
dirk@1116
   521
dirk@1811
   522
        if let addresses = pantomimeMessage.recipients() as? [CWInternetAddress] {
dirk@1811
   523
            let tos: NSMutableOrderedSet = []
dirk@1811
   524
            let ccs: NSMutableOrderedSet = []
dirk@1811
   525
            let bccs: NSMutableOrderedSet = []
dirk@1811
   526
            for addr in addresses {
dirk@1811
   527
                switch addr.type() {
dirk@1811
   528
                case .toRecipient:
dirk@1811
   529
                    tos.add(cdIdentity(pantomimeAddress: addr))
dirk@1811
   530
                case .ccRecipient:
dirk@1811
   531
                    ccs.add(cdIdentity(pantomimeAddress: addr))
dirk@1811
   532
                case .bccRecipient:
dirk@1811
   533
                    bccs.add(cdIdentity(pantomimeAddress: addr))
dirk@1811
   534
                default:
dirk@1811
   535
                    Log.warn(
dirk@1811
   536
                        component: "Message",
dirk@1811
   537
                        content: "Unsupported recipient type \(addr.type()) for \(addr.address())")
dirk@1811
   538
                }
dirk@1116
   539
            }
dirk@1811
   540
            mail.to = tos
dirk@1811
   541
            mail.cc = ccs
dirk@1811
   542
            mail.bcc = bccs
dirk@1116
   543
        }
dirk@1116
   544
dirk@1587
   545
        let referenceStrings = MutableOrderedSet<String>()
dirk@1587
   546
        if let pantomimeRefs = pantomimeMessage.allReferences() as? [String] {
dirk@1116
   547
            for ref in pantomimeRefs {
dirk@1587
   548
                referenceStrings.append(ref)
dirk@1116
   549
            }
dirk@1116
   550
        }
dirk@1116
   551
        // Append inReplyTo to references (https://cr.yp.to/immhf/thread.html)
dirk@1539
   552
        if let inReplyTo = pantomimeMessage.inReplyTo() {
dirk@1587
   553
            referenceStrings.append(inReplyTo)
dirk@1116
   554
        }
dirk@1116
   555
dirk@1594
   556
        mail.replace(referenceStrings: referenceStrings.array)
dirk@1594
   557
dirk@2451
   558
        let imap = mail.imapFields()
dirk@1116
   559
dirk@1539
   560
        imap.contentType = pantomimeMessage.contentType()
dirk@1116
   561
andreas@4451
   562
        // If the cdMessage contains attachments already, it is not a new- but an updated mail that
andreas@3381
   563
        // accidentally made its way until here.
andreas@3381
   564
        // Do *not* add the attachments again.
andreas@3381
   565
        if !containsAttachments(cdMessage: mail) {
andreas@3381
   566
            if forceParseAttachments || mail.bodyFetched {
andreas@3381
   567
                // Parsing attachments only makes sense once pantomime has received the
andreas@3381
   568
                // mail body. Same goes for the snippet.
andreas@6241
   569
                addAttachmentsFromPantomimePart(pantomimeMessage, targetMail: mail)
andreas@3381
   570
            }
dirk@1539
   571
        }
dirk@1539
   572
dirk@3027
   573
        store(headerFieldNames: ["X-pEp-Version", "X-EncStatus", "X-KeyList"],
dirk@3027
   574
              pantomimeMessage: pantomimeMessage, cdMessage: mail)
dirk@3027
   575
andreas@4451
   576
dirk@1539
   577
        Record.saveAndWait()
dirk@1564
   578
        if mail.pEpRating != PEPUtil.pEpRatingNone,
dirk@1564
   579
            let msg = mail.message() {
andreas@3153
   580
            MessageModelConfig.messageFolderDelegate?.didCreate(messageFolder: msg)
dirk@1116
   581
        }
dirk@1116
   582
dirk@1116
   583
        return mail
dirk@1116
   584
    }
andreas@3381
   585
    
andreas@3381
   586
    static private func containsAttachments(cdMessage: CdMessage) -> Bool {
andreas@3381
   587
        guard let attachments = cdMessage.attachments else {
andreas@3381
   588
            return false
andreas@3381
   589
        }
andreas@3381
   590
        return attachments.count > 0
andreas@3381
   591
    }
dirk@1116
   592
dirk@3027
   593
    static func store(headerFieldNames: [String], pantomimeMessage: CWIMAPMessage,
dirk@3027
   594
                      cdMessage: CdMessage) {
dirk@3027
   595
        var headerFields = [CdHeaderField]()
dirk@3027
   596
        for headerName in headerFieldNames {
dirk@3027
   597
            if let value = pantomimeMessage.headerValue(forName: headerName) as? String {
andreas@3416
   598
                let hf = CdHeaderField.create()
dirk@3027
   599
                hf.name = headerName
dirk@3027
   600
                hf.value = value
dirk@3027
   601
                hf.message = cdMessage
dirk@3027
   602
                headerFields.append(hf)
dirk@3027
   603
            }
dirk@3027
   604
        }
dirk@3054
   605
dirk@3054
   606
        if !headerFields.isEmpty {
dirk@3054
   607
            cdMessage.optionalFields = NSOrderedSet(array: headerFields)
dirk@3054
   608
        } else {
dirk@3054
   609
            cdMessage.optionalFields = nil
dirk@3054
   610
        }
dirk@3053
   611
        CdHeaderField.deleteOrphans()
dirk@3027
   612
    }
dirk@3027
   613
xavier@2938
   614
    static private func moreMessagesThanRequested(mail: CdMessage, messageUpdate: CWMessageUpdate) -> Bool {
xavier@2938
   615
        return mail.pEpRating != PEPUtil.pEpRatingNone && messageUpdate.rfc822
xavier@2938
   616
    }
xavier@2938
   617
dirk@1116
   618
    /**
andreas@2689
   619
     Will match existing messages.
dirk@1646
   620
     Message ID alone is not sufficient, trashed emails can and will exist in more than one folder.
dirk@1116
   621
     - Returns: An existing message that matches the given pantomime one.
dirk@1116
   622
     */
andreas@2896
   623
    static private func existing(pantomimeMessage: CWIMAPMessage, inAccount account: CdAccount) -> CdMessage? {
andreas@2896
   624
        return search(message:pantomimeMessage, inAccount: account)
dirk@1116
   625
    }
dirk@1116
   626
andreas@2689
   627
    /// Try to get the best possible match possible for given data.
andreas@2689
   628
    /// Best match priority:
andreas@2689
   629
    /// 1) UID + foldername
andreas@2689
   630
    /// 2) UUID + foldername
andreas@2689
   631
    /// 3) UUID only
andreas@2689
   632
    ///
andreas@2689
   633
    /// - Parameter message: message to search for
andreas@2689
   634
    /// - Returns: existing message
andreas@2896
   635
    static func search(message: CWIMAPMessage, inAccount account: CdAccount) -> CdMessage? {
andreas@2689
   636
        let uid = Int32(message.uid())
andreas@2905
   637
        return search(uid: uid, uuid: message.messageID(),
andreas@2905
   638
                      folderName: message.folder()?.name(), inAccount: account)
andreas@2689
   639
    }
andreas@2689
   640
dirk@1811
   641
    static func cdIdentity(pantomimeAddress: CWInternetAddress) -> CdIdentity {
dirk@2857
   642
        let theEmail = pantomimeAddress.address().fullyUnquoted()
dirk@2857
   643
        let userName = pantomimeAddress.personal()?.fullyUnquoted()
dirk@2857
   644
andreas@3326
   645
        var identity: Identity
andreas@3326
   646
        if let existing = Identity.by(address: theEmail) {
andreas@3326
   647
            identity = existing
andreas@3326
   648
            if !identity.isMySelf {
andreas@3326
   649
                identity.userName = userName
andreas@3326
   650
            }
dirk@2857
   651
        } else {
andreas@3326
   652
            // this identity has to be created
andreas@3326
   653
            identity = Identity.create(address: theEmail, userName: userName)
xavier@2800
   654
        }
andreas@3326
   655
        identity.save()
andreas@3326
   656
andreas@3326
   657
        guard let result = CdIdentity.search(address: theEmail) else {
andreas@3326
   658
            Log.shared.errorAndCrash(component: #function,
andreas@3326
   659
                                     errorString: "We have just saved this identity. It has to exist.")
andreas@3326
   660
            return CdIdentity.create()
andreas@3326
   661
        }
andreas@3326
   662
andreas@3326
   663
        return result
dirk@1116
   664
    }
dirk@1116
   665
dirk@1116
   666
    /**
dirk@1500
   667
     Adds pantomime attachments to a `CdMessage`.
dirk@1116
   668
     */
andreas@4295
   669
    static func addAttachmentsFromPantomimePart(
andreas@6241
   670
        _ part: CWPart, targetMail: CdMessage, level: Int = 0) {
dirk@1502
   671
        Log.info(component: #function, content: "Parsing level \(level) \(part)")
dirk@1116
   672
        guard let content = part.content() else {
dirk@1116
   673
            return
dirk@1116
   674
        }
dirk@1116
   675
dirk@1116
   676
        let isText = part.contentType()?.lowercased() == Constants.contentTypeText
dirk@1116
   677
        let isHtml = part.contentType()?.lowercased() == Constants.contentTypeHtml
dirk@1116
   678
        var contentData: Data?
dirk@1116
   679
        if let message = content as? CWMessage {
dirk@1116
   680
            contentData = message.dataValue()
dirk@1116
   681
        } else if let string = content as? NSString {
dirk@1116
   682
            contentData = string.data(using: String.Encoding.ascii.rawValue)
dirk@1116
   683
        } else if let data = content as? Data {
dirk@1116
   684
            contentData = data
dirk@1116
   685
        }
andreas@4451
   686
dirk@1116
   687
        if let data = contentData {
dirk@1780
   688
            if level == 0 && !isText && !isHtml && targetMail.longMessage == nil &&
dirk@1780
   689
                MiscUtil.isEmptyString(part.filename()) {
dirk@1780
   690
                // some content with unknown content type at the first level must be text
dirk@1780
   691
                targetMail.longMessage = data.toStringWithIANACharset(part.charset())
dirk@2889
   692
            } else if isText && targetMail.longMessage == nil &&
dirk@1116
   693
                MiscUtil.isEmptyString(part.filename()) {
dirk@1116
   694
                targetMail.longMessage = data.toStringWithIANACharset(part.charset())
dirk@2889
   695
            } else if isHtml && targetMail.longMessageFormatted == nil &&
dirk@1116
   696
                MiscUtil.isEmptyString(part.filename()) {
dirk@1116
   697
                targetMail.longMessageFormatted = data.toStringWithIANACharset(part.charset())
dirk@1116
   698
            } else {
andreas@6241
   699
                // we
andreas@4451
   700
                let contentDispRawValue =
andreas@4451
   701
                    CdAttachment.contentDispositionRawValue(from: part.contentDisposition())
andreas@4289
   702
                let attachment = insertAttachment(contentType: part.contentType(),
andreas@4289
   703
                                                  filename: part.filename(),
andreas@4289
   704
                                                  contentID: part.contentID(),
andreas@4451
   705
                                                  data: data,
andreas@4451
   706
                                                  contentDispositionRawValue: contentDispRawValue)
andreas@3015
   707
                targetMail.addAttachment(cdAttachment: attachment)
dirk@1116
   708
            }
dirk@1116
   709
        }
andreas@4451
   710
dirk@1116
   711
        if let multiPart = content as? CWMIMEMultipart {
dirk@1116
   712
            for i in 0..<multiPart.count() {
dirk@1116
   713
                let subPart = multiPart.part(at: UInt(i))
dirk@1116
   714
                addAttachmentsFromPantomimePart(subPart, targetMail: targetMail, level: level + 1)
dirk@1116
   715
            }
dirk@1116
   716
        }
dirk@1116
   717
    }
dirk@1116
   718
}