From 9974f4d663c657f62a6c43ec67986c999c581866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 26 Mar 2024 19:08:55 +0100 Subject: [PATCH 1/6] feat: Insert URLs directly in the draft email body for arbitrary webloc payloads --- .../Attachments/AttachmentsManager.swift | 8 +-- .../ComposeMessageIntentView.swift | 4 +- .../New Message/ComposeMessageView.swift | 9 ++- .../AttachmentsManagerWorker.swift | 7 ++- .../ComposeMessageWrapperView.swift | 22 +++++-- .../WeblocToTextAttachment.swift | 61 +++++++++++++++++++ 6 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 MailShareExtension/WeblocToTextAttachment.swift diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift index ca20ac81f..28fa6b6c1 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -116,12 +116,8 @@ import SwiftUI await worker.completeUploadedAttachments() } - func processTextAttachment(_ attachment: TextAttachable?) async { - guard let attachment else { - return - } - - await worker.processTextAttachment(attachment) + func processTextAttachments(_ attachments: [TextAttachable]) async { + await worker.processTextAttachments(attachments) } func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask { diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift index d39cd9c5b..9dd9a3e5c 100644 --- a/Mail/Views/New Message/ComposeMessageIntentView.swift +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -39,7 +39,7 @@ struct ComposeMessageIntentView: View, IntentViewable { } let composeMessageIntent: ComposeMessageIntent - var textAttachment: TextAttachable? + var textAttachments: [TextAttachable] = [] var attachments: [Attachable] = [] var body: some View { @@ -50,7 +50,7 @@ struct ComposeMessageIntentView: View, IntentViewable { mailboxManager: resolvedIntent.mailboxManager, messageReply: resolvedIntent.messageReply, attachments: attachments, - textAttachment: textAttachment + textAttachments: textAttachments ) .environmentObject(resolvedIntent.mailboxManager) } else { diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index cb2dc9b1d..02909d6b4 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -101,7 +101,7 @@ struct ComposeMessageView: View { private let messageReply: MessageReply? private let draftContentManager: DraftContentManager private let mailboxManager: MailboxManager - private let textAttachment: TextAttachable? + private let textAttachments: [TextAttachable] private var isSendButtonDisabled: Bool { let disabledState = draft.identityId == nil @@ -118,9 +118,10 @@ struct ComposeMessageView: View { mailboxManager: MailboxManager, messageReply: MessageReply? = nil, attachments: [Attachable] = [], - textAttachment: TextAttachable? = nil + textAttachments: [TextAttachable] = [] ) { self.messageReply = messageReply + self.textAttachments = textAttachments _draft = ObservedRealmObject(wrappedValue: draft) @@ -131,8 +132,6 @@ struct ComposeMessageView: View { ) draftContentManager = currentDraftContentManager - self.textAttachment = textAttachment - self.mailboxManager = mailboxManager _attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draftLocalUUID: draft.localUUID, mailboxManager: mailboxManager)) @@ -224,7 +223,7 @@ struct ComposeMessageView: View { currentSignature = try await draftContentManager.prepareCompleteDraft() async let _ = attachmentsManager.completeUploadedAttachments() - async let _ = attachmentsManager.processTextAttachment(textAttachment) + async let _ = attachmentsManager.processTextAttachments(textAttachments) isLoadingContent = false } catch { diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 637f3f38d..3a13d8fef 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -370,7 +370,12 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { await updateDelegate?.contentWillChange() } - public func processTextAttachment(_ attachment: TextAttachable) async { + public func processTextAttachments(_ attachments: [TextAttachable]) async { + // TODO: handle all the attachments + guard let attachment = attachments.first else { + return + } + // process attachment let textAttachment = await attachment.textAttachment diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 09bb5c79a..163a373e2 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -38,15 +38,29 @@ struct ComposeMessageWrapperView: View { static let typePropertyList = String(kUTTypePropertyList) - var textAttachment: TextAttachable? { - itemProviders.first { itemProvider in + /// All the Attachments that should directly provide URLs and title for a new Email Draft + var textAttachments: [TextAttachable] { + // All property list, result from JS execution in Safari + let propertyListItems = itemProviders.filter { itemProvider in itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList) } + + // All `.webloc` item providers, wrapped in a type that can read it on the fly + let weblocTextAttachments = itemProviders + .filter { $0.underlyingType == .isURL } + .compactMap { WeblocToTextAttachment(wrapping: $0) } + + let allItems: [TextAttachable] = propertyListItems + weblocTextAttachments + return allItems } + /// All the Attachments that should be uploaded as standalone files for a new Email Draft var attachments: [Attachable] { itemProviders.filter { itemProvider in - !itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList) + let isPropertyList = itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList) + let isWebloc = itemProvider.underlyingType == .isURL + + return !isPropertyList && !isWebloc } } @@ -57,7 +71,7 @@ struct ComposeMessageWrapperView: View { } else if let mailboxManager = accountManager.currentMailboxManager { ComposeMessageIntentView( composeMessageIntent: .new(originMailboxManager: mailboxManager), - textAttachment: textAttachment, + textAttachments: textAttachments, attachments: attachments ) .environmentObject(mailboxManager) diff --git a/MailShareExtension/WeblocToTextAttachment.swift b/MailShareExtension/WeblocToTextAttachment.swift new file mode 100644 index 000000000..e057ab96a --- /dev/null +++ b/MailShareExtension/WeblocToTextAttachment.swift @@ -0,0 +1,61 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation + +// import InfomaniakCore +import MailCore + +/// A wrapping type that can read an NSItemProvider that renders as a`.webloc` on the fly and provide the content thanks to the +/// `TextAttachable` protocol +struct WeblocToTextAttachment: TextAttachable { + let item: NSItemProvider + + init?(wrapping item: NSItemProvider) { + guard item.underlyingType == .isURL else { + return nil + } + + self.item = item + } + + // MARK: TextAttachable protocol + + var textAttachment: MailCore.TextAttachment { + get async { + guard let webloc = try? await item.writeToTemporaryURL() else { + return (nil, nil) + } + + guard let weblocData = NSData(contentsOf: webloc.url) else { + return (nil, nil) + } + + guard let parsedWebloc = try? PropertyListSerialization.propertyList(from: weblocData as Data, + options: [], + format: nil) as? NSDictionary else { + return (nil, nil) + } + + let parsedURL = parsedWebloc["URL"] as? String + + /// The `webloc` title is not useful for an email subject, discarding it + return TextAttachment(title: nil, body: parsedURL) + } + } +} From 0d5f8617b1d8abad8609e341ee71c02459aacab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 26 Mar 2024 19:37:08 +0100 Subject: [PATCH 2/6] feat: AttachmentsManagerWorker can process an arbitrary number of TextAttachable. --- .../AttachmentsManagerWorker.swift | 56 ++++++++++++++----- .../ComposeMessageWrapperView.swift | 4 +- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 3a13d8fef..291e2ec89 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -371,13 +371,44 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } public func processTextAttachments(_ attachments: [TextAttachable]) async { - // TODO: handle all the attachments - guard let attachment = attachments.first else { - return + // Process all text attachments + let textAttachments = await attachments.concurrentMap { attachment in + await attachment.textAttachment } - // process attachment - let textAttachment = await attachment.textAttachment + // Get first usable title + let anyUsableTitle: String = textAttachments.reduce("") { partialResult, textAttachment in + guard let title = textAttachment.title, + !title.isEmpty else { + return partialResult + } + + return title + } + + // Get all URLs + let allURLs: [String] = textAttachments.compactMap { attachment in + guard let body = attachment.body, + !body.isEmpty else { + return nil + } + + return body + } + + // Render all URLs as HTML code, if any after a minimalistic input sanitising + let formattedBodyUrls = allURLs.reduce("") { partialResult, urlString in + guard let bodyUrl = URL(string: urlString) else { + return partialResult + } + + let bodyAbsoluteUrl = bodyUrl.absoluteString + guard !bodyAbsoluteUrl.isEmpty else { + return partialResult + } + + return partialResult + "
" + bodyAbsoluteUrl + "
" + } // mutate Draft await backgroundRealm.execute { realm in @@ -389,25 +420,20 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { // Title if any usable var modified = false if draftInContext.subject.isEmpty, - let title = textAttachment.title { - draftInContext.subject = title + !anyUsableTitle.isEmpty { + draftInContext.subject = anyUsableTitle modified = true } - // Url if any after a minimalistic input sanitising - if let body = textAttachment.body, - let bodyUrl = URL(string: body), - !body.isEmpty { - draftInContext.body = "
" - + bodyUrl.absoluteString - + "
" - + draftInContext.body + if !formattedBodyUrls.isEmpty { + draftInContext.body = formattedBodyUrls + draftInContext.body modified = true } guard modified else { return } + realm.add(draftInContext, update: .modified) } } diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 163a373e2..3d3675aa3 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -58,9 +58,9 @@ struct ComposeMessageWrapperView: View { var attachments: [Attachable] { itemProviders.filter { itemProvider in let isPropertyList = itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList) - let isWebloc = itemProvider.underlyingType == .isURL + let isUrlAsWebloc = itemProvider.underlyingType == .isURL - return !isPropertyList && !isWebloc + return !isPropertyList && !isUrlAsWebloc } } From 1cd42ec59cfb70f8d48aeb1bc0d0215bd93e764e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 27 Mar 2024 08:39:32 +0100 Subject: [PATCH 3/6] refactor: Better select first usable title --- .../AttachmentsManagerWorker.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 291e2ec89..5737f110e 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -377,14 +377,14 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } // Get first usable title - let anyUsableTitle: String = textAttachments.reduce("") { partialResult, textAttachment in + let anyUsableTitle: String = textAttachments.first { textAttachment in guard let title = textAttachment.title, !title.isEmpty else { - return partialResult + return false } - return title - } + return true + }?.title ?? "" // Get all URLs let allURLs: [String] = textAttachments.compactMap { attachment in From 401b24f7a82cbec1028c1c81ce33f4b8198dd33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 27 Mar 2024 08:48:07 +0100 Subject: [PATCH 4/6] refactor: Reduce cyclomatic complexity, as hinted by SonarCloud --- .../AttachmentsManagerWorker.swift | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 5737f110e..9b0ff7b2b 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -377,38 +377,13 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } // Get first usable title - let anyUsableTitle: String = textAttachments.first { textAttachment in - guard let title = textAttachment.title, - !title.isEmpty else { - return false - } - - return true - }?.title ?? "" + let anyUsableTitle = anyUsableTitle(in: textAttachments) // Get all URLs - let allURLs: [String] = textAttachments.compactMap { attachment in - guard let body = attachment.body, - !body.isEmpty else { - return nil - } - - return body - } + let allURLs = allURLs(in: textAttachments) // Render all URLs as HTML code, if any after a minimalistic input sanitising - let formattedBodyUrls = allURLs.reduce("") { partialResult, urlString in - guard let bodyUrl = URL(string: urlString) else { - return partialResult - } - - let bodyAbsoluteUrl = bodyUrl.absoluteString - guard !bodyAbsoluteUrl.isEmpty else { - return partialResult - } - - return partialResult + "" - } + let formattedBodyUrls = formattedBodyUrls(allURLs: allURLs) // mutate Draft await backgroundRealm.execute { realm in @@ -439,6 +414,43 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } } + private func anyUsableTitle(in textAttachments: [TextAttachment]) -> String { + textAttachments.first { textAttachment in + guard let title = textAttachment.title, + !title.isEmpty else { + return false + } + + return true + }?.title ?? "" + } + + private func allURLs(in textAttachments: [TextAttachment]) -> [String] { + textAttachments.compactMap { attachment in + guard let body = attachment.body, + !body.isEmpty else { + return nil + } + + return body + } + } + + private func formattedBodyUrls(allURLs: [String]) -> String { + allURLs.reduce("") { partialResult, urlString in + guard let bodyUrl = URL(string: urlString) else { + return partialResult + } + + let bodyAbsoluteUrl = bodyUrl.absoluteString + guard !bodyAbsoluteUrl.isEmpty else { + return partialResult + } + + return partialResult + "" + } + } + @MainActor public func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask { guard let attachment = attachmentUploadTask(for: uuid) else { let finishedTask = AttachmentUploadTask() From e648d64b4ed94ea4e4196f2d1af779821e93c9a2 Mon Sep 17 00:00:00 2001 From: adrien-coye Date: Wed, 27 Mar 2024 10:29:13 +0100 Subject: [PATCH 5/6] chore: Update MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift Co-authored-by: Philippe Weidmann --- .../AttachmentsManagerWorker.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 9b0ff7b2b..ac89251fd 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -415,14 +415,7 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable { } private func anyUsableTitle(in textAttachments: [TextAttachment]) -> String { - textAttachments.first { textAttachment in - guard let title = textAttachment.title, - !title.isEmpty else { - return false - } - - return true - }?.title ?? "" + textAttachments.first { $0.title?.isEmpty == false }?.title ?? "" } private func allURLs(in textAttachments: [TextAttachment]) -> [String] { From a200217f6ac930912a0e52253c696a3a6a9492f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 27 Mar 2024 10:30:32 +0100 Subject: [PATCH 6/6] chore: PR Feedback --- MailShareExtension/WeblocToTextAttachment.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/MailShareExtension/WeblocToTextAttachment.swift b/MailShareExtension/WeblocToTextAttachment.swift index e057ab96a..d71b2e1c7 100644 --- a/MailShareExtension/WeblocToTextAttachment.swift +++ b/MailShareExtension/WeblocToTextAttachment.swift @@ -17,8 +17,6 @@ */ import Foundation - -// import InfomaniakCore import MailCore /// A wrapping type that can read an NSItemProvider that renders as a`.webloc` on the fly and provide the content thanks to the