Search code examples
iosswiftxmlgmail-apiunzip

Swift Unzipping zip file and find xml file from base64 data from Gmail API


This question is regarding a DMARC report viewer app in iOS 13 using SwiftUI and Gmail API. The reports are mailed to our admin email id by google in xml format which will be zipped. So basically it is a zip attachment. So here, GMail API is used to access those specific mail using filter and got all the base64 encoded data from API. Also decoded it to Data type data. That far is OK. Next part is where data of zip file in byte format is decompressed and extract xml file inside in String type. Then I need to parse XML. I think I can figure out parsing with XMLParser.

Question: how to decompress zip file in Data type and get xml file from it as String type?

INPUT: String in Base64 format from GMail API fetch (A zip file attachment with only 1 xml file inside)
OUTPUT: String in XML format
PLATFORM: iOS 13/Swift 5.2/SwiftUI/Xcode 11.4
ACTION: 

(INPUT)
base64: String | Decode -> Data
attachment.zip: Data | Decompress -> [Data]
ListOfFiles: [Data] | FirstIndex -> Data
dmarc.xml: Data | ContentOfXML -> String
(OUTPUT)

Update: I have tried an external package called Zip and it also failed.

let path = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let url = path.appendingPathComponent(messageId+".zip")
do {
    try data.write(to: url)
} catch {
    print("Error while writing: "+error.localizedDescription)
}
do {
    let unzipDirectory = try Zip.quickUnzipFile(url)
    print(unzipDirectory)
} catch let error as NSError {
    print("Error while unzipping: "+error.localizedDescription)
}

This code resulted in following error

Error while unzipping: The operation couldn’t be completed. (Zip.ZipError error 1.)

Solution

  • Finally I found it. As it is mentioned in Ref 1,The email bodies are encoded in 7-bit US-ASCII data. So this is why the base64 decoding did not work.

    As defined in the rfc1341:

    An encoding type of 7BIT requires that the body is already in a seven-bit mail- ready representation. This is the default value -- that is, "Content-Transfer-Encoding: 7BIT" is assumed if the Content-Transfer-Encoding header field is not present.

    The whole code worked after adding the following.

    let edata: String = result.data.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
    

    As it is mentioned in Ref 2, it just need character replacement on '-' with '+' and '_' with '/' inside base64 data received from gmail api.

    func getAttachedData(messageId: String, attachmentId: String) {
        decode(self.urlBase+messageId+"/attachments/"+attachmentId+"?"+self.urlKey) { (result: Attachment) in
            let edata: String = result.data.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
            if let data = Data(base64Encoded: edata, options: .ignoreUnknownCharacters) {
                let filemanager = FileManager.default
                let path = try! filemanager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                let url = path.appendingPathComponent(messageId+".zip")
                do {
                    try data.write(to: url)
                } catch {
                    print("Error while writing: "+error.localizedDescription)
                }
                do {
                    let unzipDirectory = try Zip.quickUnzipFile(url)
                    print("Unzipped")
                    do {
                        let filelist = try filemanager.contentsOfDirectory(at: unzipDirectory, includingPropertiesForKeys: [], options: [])
    
                        for filename in filelist {
                            print(filename.lastPathComponent)
                            print(filename.relativeString)
                            do {
                                let text = try String(contentsOf: filename, encoding: .utf8)
                                print(text)
                                DispatchQueue.main.async {
                                    self.attachments.append(text)
                                }
                            } catch let error as NSError {
                                print("Error: \(error.localizedDescription)")
                            }
                        }
                    } catch let error {
                        print("Error: \(error.localizedDescription)")
                    }
                } catch let error as NSError {
                    print("Error while unzipping: "+error.localizedDescription)
                }
            }
        }
    }
    

    Ref 1: https://stackoverflow.com/a/58590759/2382813

    Ref 2: https://stackoverflow.com/a/24986452/2382813