snipsco / Postal

A Swift framework for working with emails
MIT License
653 stars 82 forks source link

Feature request / User Story: read, manipulate and append message #53

Open ikemuc opened 7 years ago

ikemuc commented 7 years ago

Hi Kevin,

You wrote "If you want new features, please submit an issue and we'll figure out how to implement them as quickly as possible 👍" so here is what I need to do in my application next. Maybe you can help me by implementing it. Meanwhile I'll try to understand how your code works so maybe I can contribute also by expanding the Postal API to cover more/all of the libetpan API. :-)

Here is what I want to do:

"As a developer of a mail app I want to add custom headers to an IMAP message on the server and manipulate flags of an IMAP message in order to add additional information to an IMAP message."

As IMAP servers typically don't support changing an existing message, you have to:

  1. Read the message
  2. Create a new message as a clone of the original message. (Ideal would be to have something like init(messageData:...))
  3. Manipulate the new message (add/change/delete custom headers, flags, etc.)
  4. APPEND the new message in the IMAP server.
  5. Set the deleted flag of the original message
  6. Expunge the folder to delete the original message.

It would be fantastic, if you could help me as I'm stuck (tried this with mailcore2 already, but it didn't work and ) :-) (the current unsolved problem of my workaround for the problem, that there is no init() method for cloning an existing message into a new message in the MessageBuilder is [https://github.com/MailCore/mailcore2/issues/1607])

klefevre commented 7 years ago

I don't think you can modify an IMAP message like that but we can definitively try to implement a "flag setters". MailCore does something like that: https://github.com/MailCore/mailcore2/blob/2a07d182fff49f453f845298101bea7531da40f2/src/core/imap/MCIMAPSession.cpp#L1726 . Could it be what you need ?

ikemuc commented 7 years ago

Only setting flags will not help me, but it would be definitely a good first step.

For my app I need to set custom headers also like a "X-MyCustomHeader: 2017-02-25"

The code I'm using in my mailcore2 implementation looks like this. It's a bit more complicated than necessary, because the mailcore2 MessageBuilder doesn't have an init(data: msgData) method (which takes an existing message and clones it so that I can modify the message and APPEND it back on the server). So currently I'm reading the message, copying it manually to a new one in the MessageBuilder, add my custom headers and then APPEND it (adding/modifying also custom flags).

The only thing, which currently doesn't work in mailcore2 for me is, that when I copy the parts, the text message is uuencoded, so it's not human readable any more. When I use other methods to set the text (not as an attachment), the whitespaces are removed.

So yes, it's not possible to modify headers of an existing message on the IMAP server, but it's possible to create a new message and modify the custom headers and flags there and append it back on the server.

Maybe this helps you understanding, what I want to do?

Here's my current code using mailcore2:

//
// setCustomHeader()
//
func setCustomHeader(withFolder: String, uid: UInt32) -> Void {
    // 1. read complete message
    let fetchOp = imapSession.fetchMessageOperation(withFolder: withFolder, uid: uid)
    // start the IMAP operation, which is defined in fetchOp
    fetchOp?.start { (err, msgData) -> Void in
        log.debug("error from server \(err)")

        // 2. add a custom header to the message

        let messageParser: MCOMessageParser = MCOMessageParser(data: msgData)
        let messageBuilder: MCOMessageBuilder = MCOMessageBuilder()

        // copy headers
        messageBuilder.header = messageParser.header
        messageParser.header.setExtraHeaderValue("Test", forName: "X-MyCustomHeader")

        let abstractMainPart: MCOAbstractPart = messageParser.mainPart()

        if abstractMainPart.partType == MCOPartType.single {
            messageBuilder.header.removeExtraHeader(forName: "Content-Type")
            let attachment: MCOAttachment = abstractMainPart as! MCOAttachment
            messageBuilder.addAttachment(attachment)
        }
        else if abstractMainPart.partType == MCOPartType.multipartMixed {
            // remove header "Content-Type: multipart/mixed;boundary="------------XXXXXX", otherwise attachments are not shown...
            messageBuilder.header.removeExtraHeader(forName: "Content-Type")

            let multipartMixedPart: MCOMultipart = abstractMainPart as! MCOMultipart
            log.debug("multipartMixedPart: \(multipartMixedPart)\n")
            for part in multipartMixedPart.parts {
                log.debug("part: \(part)")
                let attachment: MCOAttachment = part as! MCOAttachment
                log.debug("attachment.mimeType: \(attachment.mimeType)")
                log.debug("attachment.decodedString(): \(attachment.decodedString())")
                messageBuilder.addAttachment(attachment)
            }
        }

        // 3. write copy of complete message
        let appendOp = self.imapSession.appendMessageOperation(withFolder: withFolder, messageData: messageBuilder.data(), flags: MCOMessageFlag(rawValue: 0), customFlags: ["$label1"])
        appendOp?.start{ (err, createdUid) in
            log.debug("error from server \(err)")
            log.debug("created uid: \(createdUid)")
        }
    }
ikemuc commented 7 years ago

Hi Kevin,

as my project makes steps forward I now would need to have a method to APPEND a new message. Are there plans to develop more IMAP functionality in Postal? (as the last activity is some months ago...) Currently I'm building my software on top of Postal...

The other option is that I learn how Postal is built and start developing more functionality. But as I'm not a Swift expert, I think some help from you to get into the code would be fine ;-)

klefevre commented 7 years ago

Hi @ikemuc , first of all, I apologize for not having answering you before.

Unfortunately, we don't have any plan to add new functionalities to Postal since here at Snips, we are focus on our embedded speech recognition platform (go on https://snips.ai/ and https://console.snips.ai/ for more info ).

We'll add support for Swift 4, and eventually fix bugs users would submit but that's it.

If you want to add new functionalities, I will review your PRs with pleasure.

To understand how Postal was built, you have to know that Postal is based on, libetpan, a C library that manage IMAP for us. Postal is "only" a wrapper of this library, exactly like MailCore by the way.

Postal has exactly the same features as MailCore, we only kept what we thought was the most useful for us at the time we wrote it. Keeping it stupid simple was the primary goal. Also the simplicity of the code since Postal is build in Swift whereas MailCore is a mix of C, C++ and Objective-C.

Look this implementation in Postal: https://github.com/snipsco/Postal/blob/master/Postal/IMAPSession%2BFolder.swift#L39

And now this one in MailCore: https://github.com/MailCore/mailcore2/blob/65bc5a2c045c38b35709964be614f5ceabc637ed/src/core/imap/MCIMAPSession.cpp#L1587

It's exactly the same thing but one is in swift and the other in C++.

If you wan to ingrate your feature, you should convert this method from MailCore: https://github.com/MailCore/mailcore2/blob/65bc5a2c045c38b35709964be614f5ceabc637ed/src/core/imap/MCIMAPSession.cpp#L1731 in Swift.

ikemuc commented 7 years ago

OK, thank you for your update! I'll try to add the functionality that I need :-)