/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsString.h" #include "nsCOMPtr.h" #include "nsISupportsPrimitives.h" #include "nsIImportService.h" #include "nsIImportMailboxDescriptor.h" #include "nsIImportGeneric.h" #include "nsIDirectoryEnumerator.h" #include "nsIFile.h" #include "nsIStringBundle.h" #include "nsIMsgFolder.h" #include "nsIMsgHdr.h" #include "nsIMsgPluggableStore.h" #include "nsNetUtil.h" #include "nsMsgUtils.h" #include "mozilla/Components.h" #include "nsEmlxHelperUtils.h" #include "nsAppleMailImport.h" #include "nsIOutputStream.h" // some hard-coded strings #define DEFAULT_MAIL_FOLDER "~/Library/Mail/" #define POP_MBOX_SUFFIX ".mbox" #define IMAP_MBOX_SUFFIX ".imapmbox" // stringbundle URI #define APPLEMAIL_MSGS_URL \ "chrome://messenger/locale/appleMailImportMsgs.properties" // magic constants #define kAccountMailboxID 1234 nsAppleMailImportModule::nsAppleMailImportModule() { IMPORT_LOG0("nsAppleMailImportModule Created"); nsCOMPtr bundleService = mozilla::components::StringBundle::Service(); if (bundleService) bundleService->CreateBundle(APPLEMAIL_MSGS_URL, getter_AddRefs(mBundle)); } nsAppleMailImportModule::~nsAppleMailImportModule() { IMPORT_LOG0("nsAppleMailImportModule Deleted"); } NS_IMPL_ISUPPORTS(nsAppleMailImportModule, nsIImportModule) NS_IMETHODIMP nsAppleMailImportModule::GetName(char16_t** aName) { if (!mBundle) { return NS_ERROR_FAILURE; } nsAutoString name; nsresult rv = mBundle->GetStringFromName("ApplemailImportName", name); NS_ENSURE_SUCCESS(rv, rv); *aName = ToNewUnicode(name); return rv; } NS_IMETHODIMP nsAppleMailImportModule::GetDescription(char16_t** aName) { if (!mBundle) { return NS_ERROR_FAILURE; } nsAutoString name; nsresult rv = mBundle->GetStringFromName("ApplemailImportDescription", name); NS_ENSURE_SUCCESS(rv, rv); *aName = ToNewUnicode(name); return rv; } NS_IMETHODIMP nsAppleMailImportModule::GetSupports(char** aSupports) { NS_ENSURE_ARG_POINTER(aSupports); *aSupports = strdup(NS_IMPORT_MAIL_STR); return NS_OK; } NS_IMETHODIMP nsAppleMailImportModule::GetSupportsUpgrade(bool* aUpgrade) { NS_ENSURE_ARG_POINTER(aUpgrade); *aUpgrade = false; return NS_OK; } NS_IMETHODIMP nsAppleMailImportModule::GetImportInterface( const char* aImportType, nsISupports** aInterface) { NS_ENSURE_ARG_POINTER(aImportType); NS_ENSURE_ARG_POINTER(aInterface); *aInterface = nullptr; nsresult rv = NS_ERROR_NOT_AVAILABLE; if (!strcmp(aImportType, "mail")) { nsCOMPtr mail( do_CreateInstance(NS_APPLEMAILIMPL_CONTRACTID, &rv)); if (NS_SUCCEEDED(rv)) { nsCOMPtr impSvc( do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv)); if (NS_SUCCEEDED(rv)) { nsCOMPtr generic; rv = impSvc->CreateNewGenericMail(getter_AddRefs(generic)); if (NS_SUCCEEDED(rv)) { nsAutoString name; rv = mBundle->GetStringFromName("ApplemailImportName", name); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr nameString( do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); nameString->SetData(name); generic->SetData("name", nameString); generic->SetData("mailInterface", mail); generic.forget(aInterface); } } } } return rv; } #pragma mark - nsAppleMailImportMail::nsAppleMailImportMail() : mProgress(0), mCurDepth(0) { IMPORT_LOG0("nsAppleMailImportMail created"); } nsresult nsAppleMailImportMail::Initialize() { nsCOMPtr bundleService = mozilla::components::StringBundle::Service(); NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); return bundleService->CreateBundle(APPLEMAIL_MSGS_URL, getter_AddRefs(mBundle)); } nsAppleMailImportMail::~nsAppleMailImportMail() { IMPORT_LOG0("nsAppleMailImportMail destroyed"); } NS_IMPL_ISUPPORTS(nsAppleMailImportMail, nsIImportMail) NS_IMETHODIMP nsAppleMailImportMail::GetDefaultLocation(nsIFile** aLocation, bool* aFound, bool* aUserVerify) { NS_ENSURE_ARG_POINTER(aFound); NS_ENSURE_ARG_POINTER(aLocation); NS_ENSURE_ARG_POINTER(aUserVerify); *aLocation = nullptr; *aFound = false; *aUserVerify = true; // try to find current user's top-level Mail folder nsCOMPtr mailFolder(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); if (mailFolder) { nsresult rv = mailFolder->InitWithNativePath(nsLiteralCString(DEFAULT_MAIL_FOLDER)); if (NS_SUCCEEDED(rv)) { *aFound = true; *aUserVerify = false; mailFolder.forget(aLocation); } } return NS_OK; } // this is the method that initiates all searching for mailboxes. // it will assume that it has a directory like ~/Library/Mail/ NS_IMETHODIMP nsAppleMailImportMail::FindMailboxes( nsIFile* aMailboxFile, nsTArray>& boxes) { NS_ENSURE_ARG_POINTER(aMailboxFile); IMPORT_LOG0("FindMailboxes for Apple mail invoked"); boxes.Clear(); bool exists = false; nsresult rv = aMailboxFile->Exists(&exists); if (NS_FAILED(rv) || !exists) return NS_ERROR_FAILURE; nsCOMPtr importService( do_GetService(NS_IMPORTSERVICE_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); mCurDepth = 1; // 1. look for accounts with mailboxes FindAccountMailDirs(aMailboxFile, boxes, importService); mCurDepth--; if (NS_SUCCEEDED(rv)) { // 2. look for "global" mailboxes, that don't belong to any specific // account. they are inside the // root's Mailboxes/ folder nsCOMPtr mailboxesDir( do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); if (NS_SUCCEEDED(rv)) { mailboxesDir->InitWithFile(aMailboxFile); rv = mailboxesDir->Append(u"Mailboxes"_ns); if (NS_SUCCEEDED(rv)) { IMPORT_LOG0("Looking for global Apple mailboxes"); mCurDepth++; rv = FindMboxDirs(mailboxesDir, boxes, importService); mCurDepth--; } } } return rv; } // operates on the Mail/ directory root, trying to find accounts (which are // folders named something like "POP-hwaara@gmail.com") and add their .mbox dirs void nsAppleMailImportMail::FindAccountMailDirs( nsIFile* aRoot, nsTArray>& aMailboxDescs, nsIImportService* aImportService) { nsCOMPtr directoryEnumerator; nsresult rv = aRoot->GetDirectoryEntries(getter_AddRefs(directoryEnumerator)); if (NS_FAILED(rv)) return; bool hasMore = false; while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) && hasMore) { // get the next file entry nsCOMPtr currentEntry; directoryEnumerator->GetNextFile(getter_AddRefs(currentEntry)); if (!currentEntry) continue; // make sure it's a directory bool isDirectory = false; currentEntry->IsDirectory(&isDirectory); if (isDirectory) { // now let's see if it's an account folder. if so, we want to traverse it // for .mbox children nsAutoString folderName; currentEntry->GetLeafName(folderName); bool isAccountFolder = false; if (StringBeginsWith(folderName, u"POP-"_ns)) { // cut off "POP-" prefix so we get a nice folder name folderName.Cut(0, 4); isAccountFolder = true; } else if (StringBeginsWith(folderName, u"IMAP-"_ns)) { // cut off "IMAP-" prefix so we get a nice folder name folderName.Cut(0, 5); isAccountFolder = true; } if (isAccountFolder) { IMPORT_LOG1("Found account: %s\n", NS_ConvertUTF16toUTF8(folderName).get()); // create a mailbox for this account, so we get a parent for "Inbox", // "Sent Messages", etc. nsCOMPtr desc; rv = aImportService->CreateNewMailboxDescriptor(getter_AddRefs(desc)); if (NS_FAILED(rv)) continue; desc->SetSize(1); desc->SetDepth(mCurDepth); desc->SetDisplayName(folderName.get()); desc->SetIdentifier(kAccountMailboxID); nsCOMPtr mailboxDescFile; rv = desc->GetFile(getter_AddRefs(mailboxDescFile)); if (NS_FAILED(rv) || !mailboxDescFile) continue; mailboxDescFile->InitWithFile(currentEntry); // add this mailbox descriptor to the list aMailboxDescs.AppendElement(desc); // now add all the children mailboxes mCurDepth++; FindMboxDirs(currentEntry, aMailboxDescs, aImportService); mCurDepth--; } } } } // adds the specified file as a mailboxdescriptor to the array nsresult nsAppleMailImportMail::AddMboxDir( nsIFile* aFolder, nsTArray>& aMailboxDescs, nsIImportService* aImportService) { nsAutoString folderName; aFolder->GetLeafName(folderName); // cut off the suffix, if any, or prefix if this is an account folder. if (StringEndsWith(folderName, NS_LITERAL_STRING_FROM_CSTRING(POP_MBOX_SUFFIX))) folderName.SetLength(folderName.Length() - 5); else if (StringEndsWith(folderName, NS_LITERAL_STRING_FROM_CSTRING(IMAP_MBOX_SUFFIX))) folderName.SetLength(folderName.Length() - 9); else if (StringBeginsWith(folderName, u"POP-"_ns)) folderName.Cut(4, folderName.Length()); else if (StringBeginsWith(folderName, u"IMAP-"_ns)) folderName.Cut(5, folderName.Length()); nsCOMPtr desc; nsresult rv = aImportService->CreateNewMailboxDescriptor(getter_AddRefs(desc)); if (NS_SUCCEEDED(rv)) { // find out number of messages in this .mbox uint32_t numMessages = 0; { // move to the .mbox's Messages folder nsCOMPtr messagesFolder; aFolder->Clone(getter_AddRefs(messagesFolder)); nsresult rv = messagesFolder->Append(u"Messages"_ns); NS_ENSURE_SUCCESS(rv, rv); // count the number of messages in this folder. it sucks that we have to // iterate through the folder but XPCOM doesn't give us any way to just // get the file count, unfortunately. :-( nsCOMPtr dirEnumerator; messagesFolder->GetDirectoryEntries(getter_AddRefs(dirEnumerator)); if (dirEnumerator) { bool hasMore = false; while (NS_SUCCEEDED(dirEnumerator->HasMoreElements(&hasMore)) && hasMore) { nsCOMPtr file; dirEnumerator->GetNextFile(getter_AddRefs(file)); if (file) { bool isFile = false; file->IsFile(&isFile); if (isFile) numMessages++; } } } } desc->SetSize(numMessages); desc->SetDisplayName(folderName.get()); desc->SetDepth(mCurDepth); IMPORT_LOG3("Will import %s with approx %d messages, depth is %d", NS_ConvertUTF16toUTF8(folderName).get(), numMessages, mCurDepth); // XXX: this is silly. there's no setter for the mailbox descriptor's file, // so we need to get it, and then modify it. nsCOMPtr mailboxDescFile; rv = desc->GetFile(getter_AddRefs(mailboxDescFile)); NS_ENSURE_SUCCESS(rv, rv); if (mailboxDescFile) mailboxDescFile->InitWithFile(aFolder); // add this mailbox descriptor to the list aMailboxDescs.AppendElement(desc); } return NS_OK; } // Starts looking for .mbox dirs in the specified dir. The .mbox dirs contain // messages and can be considered leafs in a tree of nested mailboxes // (subfolders). // // If a mailbox has sub-mailboxes, they are contained in a sibling folder with // the same name without the ".mbox" part. example: // MyParentMailbox.mbox/ // MyParentMailbox/ // MyChildMailbox.mbox/ // MyOtherChildMailbox.mbox/ // nsresult nsAppleMailImportMail::FindMboxDirs( nsIFile* aFolder, nsTArray>& aMailboxDescs, nsIImportService* aImportService) { NS_ENSURE_ARG_POINTER(aFolder); NS_ENSURE_ARG_POINTER(aImportService); // make sure this is a directory. bool isDir = false; if (NS_FAILED(aFolder->IsDirectory(&isDir)) || !isDir) return NS_ERROR_FAILURE; // iterate through the folder contents nsCOMPtr directoryEnumerator; nsresult rv = aFolder->GetDirectoryEntries(getter_AddRefs(directoryEnumerator)); if (NS_FAILED(rv) || !directoryEnumerator) return rv; bool hasMore = false; while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) && hasMore) { // get the next file entry nsCOMPtr currentEntry; directoryEnumerator->GetNextFile(getter_AddRefs(currentEntry)); if (!currentEntry) continue; // we only care about directories... if (NS_FAILED(currentEntry->IsDirectory(&isDir)) || !isDir) continue; // now find out if this is a .mbox dir nsAutoString currentFolderName; if (NS_SUCCEEDED(currentEntry->GetLeafName(currentFolderName)) && (StringEndsWith(currentFolderName, NS_LITERAL_STRING_FROM_CSTRING(POP_MBOX_SUFFIX)) || StringEndsWith(currentFolderName, NS_LITERAL_STRING_FROM_CSTRING(IMAP_MBOX_SUFFIX)))) { IMPORT_LOG1("Adding .mbox dir: %s", NS_ConvertUTF16toUTF8(currentFolderName).get()); // add this .mbox rv = AddMboxDir(currentEntry, aMailboxDescs, aImportService); if (NS_FAILED(rv)) { IMPORT_LOG1("Couldn't add .mbox for import: %s ... continuing anyway", NS_ConvertUTF16toUTF8(currentFolderName).get()); continue; } // see if this .mbox dir has any sub-mailboxes nsAutoString siblingMailboxDirPath; currentEntry->GetPath(siblingMailboxDirPath); // cut off suffix if (StringEndsWith(siblingMailboxDirPath, NS_LITERAL_STRING_FROM_CSTRING(IMAP_MBOX_SUFFIX))) siblingMailboxDirPath.SetLength(siblingMailboxDirPath.Length() - 9); else if (StringEndsWith(siblingMailboxDirPath, NS_LITERAL_STRING_FROM_CSTRING(POP_MBOX_SUFFIX))) siblingMailboxDirPath.SetLength(siblingMailboxDirPath.Length() - 5); IMPORT_LOG1("trying to locate a '%s'", NS_ConvertUTF16toUTF8(siblingMailboxDirPath).get()); nsCOMPtr siblingMailboxDir( do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); if (NS_FAILED(rv)) continue; rv = siblingMailboxDir->InitWithPath(siblingMailboxDirPath); bool reallyExists = false; siblingMailboxDir->Exists(&reallyExists); if (NS_SUCCEEDED(rv) && reallyExists) { IMPORT_LOG1("Found what looks like an .mbox container: %s", NS_ConvertUTF16toUTF8(currentFolderName).get()); // traverse this folder for other .mboxes mCurDepth++; FindMboxDirs(siblingMailboxDir, aMailboxDescs, aImportService); mCurDepth--; } } } return NS_OK; } NS_IMETHODIMP nsAppleMailImportMail::ImportMailbox(nsIImportMailboxDescriptor* aMailbox, nsIMsgFolder* aDstFolder, char16_t** aErrorLog, char16_t** aSuccessLog, bool* aFatalError) { nsAutoString errorLog, successLog; // reset progress mProgress = 0; nsAutoString mailboxName; aMailbox->GetDisplayName(getter_Copies(mailboxName)); nsCOMPtr mboxFolder; nsresult rv = aMailbox->GetFile(getter_AddRefs(mboxFolder)); if (NS_FAILED(rv) || !mboxFolder) { ReportStatus(u"ApplemailImportMailboxConverterror", mailboxName, errorLog); SetLogs(successLog, errorLog, aSuccessLog, aErrorLog); return NS_ERROR_FAILURE; } // if we're an account mailbox, nothing do. if we're a real mbox // then we've got some messages to import! uint32_t mailboxIdentifier; aMailbox->GetIdentifier(&mailboxIdentifier); if (mailboxIdentifier != kAccountMailboxID) { // move to the .mbox's Messages folder nsCOMPtr messagesFolder; mboxFolder->Clone(getter_AddRefs(messagesFolder)); rv = messagesFolder->Append(u"Messages"_ns); if (NS_FAILED(rv)) { // even if there are no messages, it might still be a valid mailbox, or // even a parent for other mailboxes. // // just indicate that we're done, using the same number that we used to // estimate number of messages earlier. uint32_t finalSize; aMailbox->GetSize(&finalSize); mProgress = finalSize; // report that we successfully imported this mailbox ReportStatus(u"ApplemailImportMailboxSuccess", mailboxName, successLog); SetLogs(successLog, errorLog, aSuccessLog, aErrorLog); return NS_OK; } // let's import the messages! nsCOMPtr directoryEnumerator; rv = messagesFolder->GetDirectoryEntries( getter_AddRefs(directoryEnumerator)); if (NS_FAILED(rv)) { ReportStatus(u"ApplemailImportMailboxConvertError", mailboxName, errorLog); SetLogs(successLog, errorLog, aSuccessLog, aErrorLog); return NS_ERROR_FAILURE; } // prepare an outstream to the destination file nsCOMPtr msgStore; rv = aDstFolder->GetMsgStore(getter_AddRefs(msgStore)); if (!msgStore || NS_FAILED(rv)) { ReportStatus(u"ApplemailImportMailboxConverterror", mailboxName, errorLog); SetLogs(successLog, errorLog, aSuccessLog, aErrorLog); return NS_ERROR_FAILURE; } bool hasMore = false; nsCOMPtr outStream; while (NS_SUCCEEDED(directoryEnumerator->HasMoreElements(&hasMore)) && hasMore) { // get the next file entry nsCOMPtr currentEntry; directoryEnumerator->GetNextFile(getter_AddRefs(currentEntry)); if (!currentEntry) continue; // make sure it's an .emlx file bool isFile = false; currentEntry->IsFile(&isFile); if (!isFile) continue; nsAutoString leafName; currentEntry->GetLeafName(leafName); if (!StringEndsWith(leafName, u".emlx"_ns)) continue; nsCOMPtr msgHdr; rv = msgStore->GetNewMsgOutputStream(aDstFolder, getter_AddRefs(msgHdr), getter_AddRefs(outStream)); if (NS_FAILED(rv)) break; // Add the data to the mbox stream. if (NS_SUCCEEDED(nsEmlxHelperUtils::AddEmlxMessageToStream(currentEntry, outStream))) { mProgress++; msgStore->FinishNewMessage(outStream, msgHdr); outStream = nullptr; } else { msgStore->DiscardNewMessage(outStream, msgHdr); outStream = nullptr; break; } } } // just indicate that we're done, using the same number that we used to // estimate number of messages earlier. uint32_t finalSize; aMailbox->GetSize(&finalSize); mProgress = finalSize; // report that we successfully imported this mailbox ReportStatus(u"ApplemailImportMailboxSuccess", mailboxName, successLog); SetLogs(successLog, errorLog, aSuccessLog, aErrorLog); return NS_OK; } void nsAppleMailImportMail::ReportStatus(const char16_t* aErrorName, nsString& aName, nsAString& aStream) { // get (and format, if needed) the error string from the bundle nsAutoString outString; AutoTArray fmt = {aName}; nsresult rv = mBundle->FormatStringFromName( NS_ConvertUTF16toUTF8(aErrorName).get(), fmt, outString); // write it out the stream if (NS_SUCCEEDED(rv)) { aStream.Append(outString); aStream.Append(char16_t('\n')); } } void nsAppleMailImportMail::SetLogs(const nsAString& aSuccess, const nsAString& aError, char16_t** aOutSuccess, char16_t** aOutError) { if (aOutError && !*aOutError) *aOutError = ToNewUnicode(aError); if (aOutSuccess && !*aOutSuccess) *aOutSuccess = ToNewUnicode(aSuccess); } NS_IMETHODIMP nsAppleMailImportMail::GetImportProgress(uint32_t* aDoneSoFar) { NS_ENSURE_ARG_POINTER(aDoneSoFar); *aDoneSoFar = mProgress; return NS_OK; } NS_IMETHODIMP nsAppleMailImportMail::TranslateFolderName( const nsAString& aFolderName, nsAString& aResult) { aResult = aFolderName; return NS_OK; }