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