Bug 856087 - Implement a pluggable store converter (mbox <-> maildir). r=mkmelin,aceman

This commit is contained in:
Anindya-Pandey 2017-01-07 23:52:00 +02:00
Родитель bd91833fac
Коммит f27dcea3e0
19 изменённых файлов: 1811 добавлений и 8 удалений

Просмотреть файл

@ -0,0 +1,11 @@
<!-- 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/. -->
<!ENTITY converterDialog.title "Message Store Type Converter">
<!ENTITY converterDialog.continueButton "Continue">
<!ENTITY converterDialog.cancelButton "Cancel">
<!ENTITY converterDialog.finishButton "Finish">
<!ENTITY converterDialog.complete "The conversion is complete. &brandShortName; will now restart.">
<!ENTITY converterDialog.error "Conversion failed.">

Просмотреть файл

@ -0,0 +1,41 @@
# 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/.
# LOCALIZATION NOTE (converterDialog.warning):
# %1$S will be replaced by the name of the account which is going to be converted.
# %2$S will be replaced by the format into which the account will be converted.
# %3$S will be replaced by $BrandShortName.
converterDialog.warning=The messages in the account %1$S will now be converted to the %2$S format. %3$S will restart after the conversion is complete.
# LOCALIZATION NOTE (converterDialog.message):
# %1$S will be replaced by the name of the account which is being converted.
# %2$S will be replaced by the format into which the account will be converted.
converterDialog.message=Converting the account %1$S to %2$S…
# LOCALIZATION NOTE (converterDialog.warningForDeferredAccount):
# %1$S will be replaced by the name of the deferred account for which migration is initiated by the user.
# %2$S will be replaced by the name of the account to which the deferred account is deferred ie the name of the deferred-to account.
# %3$S will be replaced by the name of the deferred-to account.
# %4$S will be replaced by a comma separated list of names of accounts which are deferred to the deferred-to account.
# %5$S will be replaced by a comma separated list of names of accounts which are going to get converted.
# %6$S will be replaced by the format into which the accounts will be converted.
# %7$S will be replaced by $BrandShortName.
converterDialog.warningForDeferredAccount=%1$S is deferred to %2$S. Accounts deferred to %3$S: %4$S. The messages in the accounts %5$S will now be converted to the %6$S format. %7$S will restart after the conversion is complete.
# LOCALIZATION NOTE (converterDialog.warningForDeferredToAccount):
# %1$S will be replaced by the name of the deferred-to account for which migration is initiated by the user and to which other accounts are deferred.
# %2$S will be replaced by a comma separated list of names of accounts which are deferred to the deferred-to account.
# %3$S will be replaced by a comma separated list of names of accounts which are going to get converted.
# %4$S will be replaced by the format into which the accounts will be converted.
# %5$S will be replaced by $BrandShortName.
converterDialog.warningForDeferredToAccount=Accounts deferred to %1$S: %2$S. The messages in the accounts %3$S will now be converted to the %4$S format. %5$S will restart after the conversion is complete.
# LOCALIZATION NOTE (converterDialog.messageForDeferredAccount):
# %1$S will be replaced by a comma separated list of names of accounts which are being converted.
# %2$S will be replaced by the format into which the accounts will be converted.
converterDialog.messageForDeferredAccount=Converting the accounts %1$S to %2$S…
# LOCALIZATION NOTE (converterDialog.percentDone):
# %1$S will be replaced by the percentage of conversion that is complete.
converterDialog.percentDone=%1$S%% done

Просмотреть файл

@ -49,6 +49,8 @@
locale/@AB_CD@/messenger/am-addressing.dtd (%chrome/messenger/am-addressing.dtd)
locale/@AB_CD@/messenger/am-main.dtd (%chrome/messenger/am-main.dtd)
locale/@AB_CD@/messenger/am-server-top.dtd (%chrome/messenger/am-server-top.dtd)
locale/@AB_CD@/messenger/converterDialog.dtd (%chrome/messenger/converterDialog.dtd)
locale/@AB_CD@/messenger/converterDialog.properties (%chrome/messenger/converterDialog.properties)
locale/@AB_CD@/messenger/am-identities-list.dtd (%chrome/messenger/am-identities-list.dtd)
locale/@AB_CD@/messenger/am-identity-edit.dtd (%chrome/messenger/am-identity-edit.dtd)
locale/@AB_CD@/messenger/am-im.dtd (%chrome/messenger/am-im.dtd)

Просмотреть файл

@ -8,6 +8,58 @@ ChromeUtils.import("resource:///modules/MailUtils.js");
ChromeUtils.import("resource://gre/modules/Services.jsm");
var gServer;
var gOriginalStoreType;
/**
* Called when the store type menu is clicked.
* @param {Object} aStoreTypeElement - store type menu list element.
*/
function clickStoreTypeMenu(aStoreTypeElement) {
if (aStoreTypeElement.value == gOriginalStoreType) {
return;
}
// Response from migration dialog modal. If the conversion is complete
// 'response.newRootFolder' will hold the path to the new account root folder,
// otherwise 'response.newRootFolder' will be null.
let response = { newRootFolder: null };
// Send 'response' as an argument to converterDialog.xhtml.
window.openDialog("converterDialog.xhtml","mailnews:mailstoreconverter",
"modal,centerscreen,width=800,height=180", gServer,
aStoreTypeElement.value, response);
changeStoreType(response);
}
/**
* Revert store type to the original store type if converter modal closes
* before migration is complete, otherwise change original store type to
* currently selected store type.
* @param {Object} aResponse - response from migration dialog modal.
*/
function changeStoreType(aResponse) {
if (aResponse.newRootFolder) {
// The conversion is complete.
// Set local path to the new account root folder which is present
// in 'aResponse.newRootFolder'.
if (gServer.type == "nntp") {
let newRootFolder = aResponse.newRootFolder;
let lastSlash = newRootFolder.lastIndexOf("/");
let newsrc = newRootFolder.slice(0, lastSlash) + "/newsrc-" +
newRootFolder.slice(lastSlash + 1);
document.getElementById("nntp.newsrcFilePath").value = newsrc;
}
document.getElementById("server.localPath").value = aResponse.newRootFolder;
gOriginalStoreType = document.getElementById("server.storeTypeMenulist")
.value;
BrowserUtils.restartApplication();
} else {
// The conversion failed or was cancelled.
// Restore selected item to what was selected before conversion.
document.getElementById("server.storeTypeMenulist").value =
gOriginalStoreType;
}
}
function onSave()
{
@ -46,9 +98,12 @@ function onInit(aPageId, aServerId)
.getAttribute("value");
let targetItem = storeTypeElement.getElementsByAttribute("value", currentStoreID);
storeTypeElement.selectedItem = targetItem[0];
// disable store type change if store has already been used
// Disable store type change if store has not been used yet.
storeTypeElement.setAttribute("disabled",
gServer.getBoolValue("canChangeStoreType") ? "false" : "true");
gServer.getBoolValue("canChangeStoreType") ?
"false" : !Services.prefs.getBoolPref("mail.store_conversion_enabled"));
// Initialise 'gOriginalStoreType' to the item that was originally selected.
gOriginalStoreType = storeTypeElement.value;
}
function onPreInit(account, accountValues)

Просмотреть файл

@ -400,7 +400,8 @@
<label value="&storeType.label;"
accesskey="&storeType.accesskey;"
control="server.storeTypeMenulist"/>
<menulist id="server.storeTypeMenulist">
<menulist id="server.storeTypeMenulist"
oncommand="clickStoreTypeMenu(this);">
<menupopup id="server.storeTypeMenupopup">
<menuitem id="server.mboxStore"
value="@mozilla.org/msgstore/berkeleystore;1"

Просмотреть файл

@ -3,7 +3,53 @@
* 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/. */
ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
var gServer;
var gOriginalStoreType;
/**
* Called when the store type menu is clicked.
* @param {Object} aStoreTypeElement - store type menu list element.
*/
function clickStoreTypeMenu(aStoreTypeElement) {
if (aStoreTypeElement.value == gOriginalStoreType) {
return;
}
// Response from migration dialog modal. If the conversion is complete
// 'response.newRootFolder' will hold the path to the new account root folder,
// otherwise 'response.newRootFolder' will be null.
let response = { newRootFolder: null };
// Send 'response' as an argument to converterDialog.xhtml.
window.openDialog("converterDialog.xhtml","mailnews:mailstoreconverter",
"modal,centerscreen,width=800,height=180", gServer,
aStoreTypeElement.value, response);
changeStoreType(response);
}
/**
* Revert store type to the original store type if converter modal closes
* before migration is complete, otherwise change original store type to
* currently selected store type.
* @param {Object} aResponse - response from migration dialog modal.
*/
function changeStoreType(aResponse) {
if (aResponse.newRootFolder) {
// The conversion is complete.
// Set local path to the new account root folder which is present
// in 'aResponse.newRootFolder'.
document.getElementById("server.localPath").value = aResponse.newRootFolder;
gOriginalStoreType = document.getElementById("server.storeTypeMenulist")
.value;
BrowserUtils.restartApplication();
} else {
// The conversion failed or was cancelled.
// Restore selected item to what was selected before conversion.
document.getElementById("server.storeTypeMenulist").value =
gOriginalStoreType;
}
}
function onInit(aPageId, aServerId) {
@ -14,9 +60,8 @@ function onInit(aPageId, aServerId) {
.getAttribute("value");
let targetItem = storeTypeElement.getElementsByAttribute("value", currentStoreID);
storeTypeElement.selectedItem = targetItem[0];
// disable store type change if store has already been used
storeTypeElement.setAttribute("disabled",
gServer.getBoolValue("canChangeStoreType") ? "false" : "true");
// Initialise 'gOriginalStoreType' to the item that was originally selected.
gOriginalStoreType = storeTypeElement.value;
}
function onPreInit(account, accountValues) {

Просмотреть файл

@ -51,7 +51,8 @@
<label value="&storeType.label;"
accesskey="&storeType.accesskey;"
control="server.storeTypeMenulist"/>
<menulist id="server.storeTypeMenulist">
<menulist id="server.storeTypeMenulist"
oncommand="clickStoreTypeMenu(this);">
<menupopup id="server.storeTypeMenupopup">
<menuitem id="server.mboxStore"
value="@mozilla.org/msgstore/berkeleystore;1"

Просмотреть файл

@ -0,0 +1,323 @@
/* 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/. */
/**
* This file contains functionality for the front-end part of the mail store
* type conversion.
*/
ChromeUtils.import("resource:///modules/iteratorUtils.jsm");
ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
ChromeUtils.import("resource:///modules/MailUtils.js");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");
ChromeUtils.import("resource://gre/modules/Log.jsm");
ChromeUtils.import("resource:///modules/mailstoreConverter.jsm");
ChromeUtils.import("resource:///modules/folderUtils.jsm");
var log = Log.repository.getLogger("MailStoreConverter");
// {nsIMsgIncomingServer} server for the account to be migrated.
var gServer;
// {nsIMsgFolder} account root folder.
var gFolder;
// 'gResponse.newRootFolder' is path to the new account root folder if migration
// is complete, else null.
// 'gResponse' is set to the modified response parameter received from
// am-server.js.
var gResponse;
// Array to hold deferred accounts.
var gDeferredAccounts = [];
// Value of Services.io.offline before migration.
var gOriginalOffline;
/**
* Place account name in migration dialog modal.
* @param {nsIMsgIncomingServer} aServer - account server.
*/
function placeAccountName(aServer) {
gOriginalOffline = Services.io.offline;
let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
.createBundle("chrome://messenger/locale/converterDialog.properties");
let brandShortName = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
.createBundle("chrome://branding/locale/brand.properties")
.GetStringFromName("brandShortName");
// 'deferredToRootFolder' holds path of rootMsgFolder of account to which
// other accounts have been deferred.
let deferredToRootFolder = aServer.rootMsgFolder.filePath.path;
// String to hold names of deferred accounts separated by commas.
let deferredAccountsString = "";
// Account to which other accounts have been deferred.
let deferredToAccount;
// Array of all accounts.
let accounts = allAccountsSorted(true);
for (let account of accounts) {
if (account.incomingServer.rootFolder.filePath.path ==
deferredToRootFolder) {
// Other accounts may be deferred to this account.
deferredToAccount = account;
} else if (account.incomingServer.rootMsgFolder.filePath.path ==
deferredToRootFolder) {
// This is a deferred account.
gDeferredAccounts.push(account);
}
}
// String to hold the names of accounts to be converted separated by commas.
let accountsToConvert = "";
if (gDeferredAccounts.length >= 1) {
// Add account names to 'accountsToConvert' and 'deferredAccountsString'.
for (let i = 0; i < gDeferredAccounts.length; i++) {
if (i < gDeferredAccounts.length - 1) {
accountsToConvert += gDeferredAccounts[i].incomingServer.username + ", ";
deferredAccountsString += gDeferredAccounts[i].incomingServer.username + ", ";
} else {
accountsToConvert += gDeferredAccounts[i].incomingServer.username;
deferredAccountsString += gDeferredAccounts[i].incomingServer.username;
}
}
// Username of Local Folders is "nobody". So it's better to use
// its hostname which is "Local Folders".
// TODO: maybe test against .key == MailServices.accounts.localFoldersServer.key ?
if (deferredToAccount.incomingServer.hostName == "Local Folders") {
accountsToConvert += ", " + deferredToAccount.incomingServer.prettyName;
} else {
accountsToConvert += ", " + deferredToAccount.incomingServer.prettyName;
}
log.info(accountsToConvert + " will be converted");
let storeContractId = Services.prefs.getCharPref("mail.server." +
deferredToAccount.incomingServer.key + ".storeContractID");
if (storeContractId == "@mozilla.org/msgstore/berkeleystore;1") {
storeContractId = "maildir";
} else {
storeContractId = "mbox";
}
// Username of Local Folders is "nobody". So it's better to use
// its hostname which is "Local Folders".
// TODO: maybe test against .key != MailServices.accounts.localFoldersServer.key ?
let deferredToAccountName = deferredToAccount.incomingServer.hostName;
if (deferredToAccountName != "Local Folders") {
deferredToAccountName = deferredToAccount.incomingServer.username;
}
if (aServer.rootFolder.filePath.path != deferredToRootFolder) {
document.getElementById("warningSpan").innerHTML =
bundle.formatStringFromName(
"converterDialog.warningForDeferredAccount",
[aServer.username, deferredToAccountName, deferredToAccountName,
deferredAccountsString,
accountsToConvert, storeContractId, brandShortName], 7);
} else {
document.getElementById("warningSpan").innerHTML =
bundle.formatStringFromName(
"converterDialog.warningForDeferredToAccount",
[deferredToAccountName,
deferredAccountsString,
accountsToConvert, storeContractId, brandShortName], 5);
}
document.getElementById("messageSpan").innerHTML =
bundle.formatStringFromName("converterDialog.messageForDeferredAccount",
[accountsToConvert, storeContractId], 2);
gServer = deferredToAccount.incomingServer;
} else {
// No account is deferred.
let storeContractId = Services.prefs.getCharPref(
"mail.server." + aServer.key + ".storeContractID");
if (storeContractId == "@mozilla.org/msgstore/berkeleystore;1") {
storeContractId = "maildir";
} else {
storeContractId = "mbox";
}
let tempName = aServer.username;
if (tempName == "nobody") {
tempName = "Local Folders";
} else if (!tempName) {
tempName = aServer.hostName;
}
document.getElementById("warningSpan").innerHTML =
bundle.formatStringFromName("converterDialog.warning",
[tempName, storeContractId, brandShortName],
3);
document.getElementById("messageSpan").innerHTML =
bundle.formatStringFromName("converterDialog.message",
[tempName, storeContractId], 2);
gServer = aServer;
}
}
/**
* Start the conversion process.
* @param {String} aSelectedStoreType - mailstore type selected by user.
* @param {Object} aResponse - response from the migration dialog modal.
*/
function startContinue(aSelectedStoreType, aResponse) {
gResponse = aResponse;
gFolder = gServer.rootFolder.filePath;
let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
.createBundle("chrome://messenger/locale/converterDialog.properties");
document.getElementById("progress").addEventListener("progress", function(e) {
document.getElementById("progress").value = e.detail;
document.getElementById("progressPrcnt").innerHTML =
bundle.formatStringFromName("converterDialog.percentDone", [e.detail], 1);
});
document.getElementById("warning").style.display = "none";
document.getElementById("progressDiv").style.display = "block";
// Storing original prefs and root folder path
// to revert changes in case of error.
let p1 = "mail.server." + gServer.key + ".directory";
let p2 = "mail.server." + gServer.key + ".directory-rel";
let p3 = "mail.server." + gServer.key + ".newsrc.file";
let p4 = "mail.server." + gServer.key + ".newsrc.file-rel";
let p5 = "mail.server." + gServer.key + ".storeContractID";
let originalDirectoryPref = Services.prefs.getCharPref(p1);
let originalDirectoryRelPref = Services.prefs.getCharPref(p2);
let originalNewsrcFilePref;
let originalNewsrcFileRelPref;
if (gServer.type == "nntp") {
originalNewsrcFilePref = Services.prefs.getCharPref(p3);
originalNewsrcFileRelPref = Services.prefs.getCharPref(p4);
}
let originalStoreContractID = Services.prefs.getCharPref(p5);
let originalRootFolderPath = gServer.rootFolder.filePath.path;
/**
* Called when promise returned by convertMailStoreTo() is rejected.
* @param {String} aReason - error because of which the promise was rejected.
*/
function promiseRejected(aReason){
log.error("Conversion to '" + mailstoreContractId + "' failed: " +
aReason);
document.getElementById("messageSpan").style.display = "none";
document.getElementById("errorSpan").style.display = "block";
gResponse.newRootFolder = null;
// Revert prefs.
Services.prefs.setCharPref(p1, originalDirectoryPref);
Services.prefs.setCharPref(p2, originalDirectoryRelPref);
if (gServer.type == "nntp") {
Services.prefs.setCharPref(p3, originalNewsrcFilePref);
Services.prefs.setCharPref(p4, originalNewsrcFileRelPref);
}
Services.prefs.setCharPref(p5, originalStoreContractID);
Services.prefs.savePrefFile(null);
if (gServer.rootFolder.filePath.path != originalRootFolderPath)
gServer.rootFolder.filePath = new FileUtils.File(originalRootFolderPath);
Services.io.offline = gOriginalOffline;
}
/**
* Called when promise returned by convertMailStoreTo() is resolved.
* @param {String} aVal - path of the new account root folder with which the
* promise returned by convertMailStoreTo() is resolved.
*/
function promiseResolved(aVal) {
let newMailstoreContractId = aSelectedStoreType;
log.info("Converted to '" + newMailstoreContractId + "' - " + aVal);
gResponse.newRootFolder = aVal;
for (let deferredAccount of gDeferredAccounts) {
let defServer = deferredAccount.incomingServer;
defServer.rootMsgFolder.filePath = new FileUtils.File(aVal);
Services.prefs.setCharPref("mail.server." + defServer.key +
".storeContractID", newMailstoreContractId);
}
Services.io.offline = gOriginalOffline;
document.getElementById("cancel").style.display = "none";
document.getElementById("finish").style.display = "inline-block";
document.getElementById("messageSpan").style.display = "none";
document.getElementById("completeSpan").style.display = "block";
}
/**
* Check whether an mbox folder can be compacted or not.
* @param {nsIMsgFolder} aFolder - mbox folder that is to be checked.
*/
function canCompact(aFolder) {
if (aFolder.expungedBytes != 0)
return true;
if (aFolder.hasSubFolders) {
let subFolders = aFolder.subFolders;
while (subFolders.hasMoreElements()) {
if (canCompact(subFolders.getNext().QueryInterface(Ci.nsIMsgFolder)))
return true;
}
}
return false;
}
// Compaction (compactAll()) works only for mbox folders which satisfy one of
// the following 2 conditions -
// 1. Messages are moved out of the folder.
// 2. Messages are moved out of some descendant folder of the folder.
// If the account root folder can be compacted, start the conversion after
// compacting it.
if (originalStoreContractID == "@mozilla.org/msgstore/berkeleystore;1"
&&
canCompact(gServer.rootFolder)) {
let urlListener = {
OnStartRunningUrl: function (aUrl) {
},
OnStopRunningUrl: function (aUrl, aExitCode) {
let pConvert = convertMailStoreTo(originalStoreContractID,
gServer, document.getElementById("progress"));
pConvert.then(function(val) {
promiseResolved(val);
}).catch(function(reason) {
promiseRejected(reason);
});
}
};
gServer.rootFolder.compactAll(urlListener, null, gServer.type == "imap" ||
gServer.type == "nntp");
}
else {
let pConvert = convertMailStoreTo(originalStoreContractID,
gServer, document.getElementById("progress"));
pConvert.then(function(val) {
promiseResolved(val);
}).catch(function(reason) {
promiseRejected(reason);
});
}
}
/**
* Cancel the conversion.
* @param {Object} aResponse - response param from the migration dialog modal.
*/
function cancelConversion(aResponse) {
gResponse = aResponse;
gResponse.newRootFolder = null;
terminateWorkers();
Services.io.offline = gOriginalOffline;
window.close();
}
/**
* Called when "finish" button is clicked.
*/
function finishConversion() {
window.close();
}

Просмотреть файл

@ -0,0 +1,63 @@
<!-- 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/.
-->
<!DOCTYPE html [
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
<!ENTITY % converterDTD SYSTEM "chrome://messenger/locale/converterDialog.dtd">
%converterDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>&converterDialog.title;</title>
<style>
.controls {
width: 97%;
position: fixed;
bottom: 20px;
text-align: right;
}
#progressDiv {
margin: 1%;
}
#progress {
width: 80%;
}
</style>
<script src="converterDialog.js" />
</head>
<body onload="placeAccountName(window.arguments[0]);">
<div id="warning">
<p id="warningSpan">[The messages in the account XXX will now be converted to the maildir format.]</p>
<div class="controls">
<button onclick="cancelConversion(window.arguments[2]);">
&converterDialog.cancelButton;
</button>
<button onclick="startContinue(window.arguments[1], window.arguments[2]);">
&converterDialog.continueButton;
</button>
</div>
</div>
<div id="progressDiv" hidden="hidden">
<p id="messageSpan">[Messages go here]</p>
<p id="completeSpan" hidden="hidden">&converterDialog.complete;</p>
<p id="errorSpan" hidden="hidden">&converterDialog.error;</p>
<p>
<progress id="progress" value="0" max="100"></progress>
<span id="progressPrcnt">0% done</span>
</p>
<div class="controls">
<button id="cancel" onclick="cancelConversion(window.arguments[1]);">
&converterDialog.cancelButton;
</button>
<button id="finish" onclick="finishConversion();" hidden="hidden">
&converterDialog.finishButton;
</button>
</div>
</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,179 @@
/* 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/. */
ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");
ChromeUtils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
ChromeUtils.import("resource:///modules/mailstoreConverter.jsm");
ChromeUtils.import("resource://gre/modules/Log.jsm");
ChromeUtils.import("resource:///modules/folderUtils.jsm");
// XXX: merge into test_converter.js
var log = Log.repository.getLogger("MailStoreConverter");
Services.prefs.setCharPref("mail.serverDefaultStoreContractID",
"@mozilla.org/msgstore/berkeleystore;1");
// No. of messages/files and folders copied.
var gProgressValue = 0;
var gMsgHdrs = [];
// {nsIMsgLocalMailFolder} folder carrying messages for the pop server.
var gInbox;
// {nsIMsgIncomingServer} server for first deferred pop account.
var gServer1;
// {nsIMsgIncomingServer} server for second deferred pop account.
var gServer2;
// {nsIMsgIncomingServer} server to convert.
var gServer;
var copyListenerWrap = {
SetMessageKey: function(aKey) {
let hdr = gInbox.GetMessageHeader(aKey);
gMsgHdrs.push({hdr: hdr, ID: hdr.messageId});
},
OnStopCopy: function(aStatus) {
// Check: message successfully copied.
Assert.equal(aStatus, 0);
}
};
var EventTarget = function () {
this.dispatchEvent = function (event) {
if (event.type == "progress") {
log.trace("Progress: " + event.detail);
}
};
};
function copyFileMessage(file, destFolder, isDraftOrTemplate)
{
let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
MailServices.copy.CopyFileMessage(file, destFolder, null, isDraftOrTemplate,
0, "", listener, null);
return listener.promise;
}
/**
* Check that conversion worked for the given source.
* @param source - mbox source directory
* @param target - maildir target directory
*/
function checkConversion(source, target) {
let sourceContents = source.directoryEntries;
while (sourceContents.hasMoreElements()) {
let sourceContent = sourceContents.getNext().QueryInterface(Ci.nsIFile);
let sourceContentName = sourceContent.leafName;
let ext = sourceContentName.substr(-4);
let targetFile = FileUtils.File(OS.Path.join(target.path,sourceContentName));
log.debug("Checking path: " + targetFile.path);
if (ext == ".dat") {
Assert.ok(targetFile.exists());
} else if (sourceContent.isDirectory()) {
Assert.ok(targetFile.exists());
checkConversion(sourceContent, targetFile);
} else if (ext != ".msf") {
Assert.ok(targetFile.exists());
let cur = FileUtils.File(OS.Path.join(targetFile.path,"cur"));
Assert.ok(cur.exists());
let tmp = FileUtils.File(OS.Path.join(targetFile.path,"tmp"));
Assert.ok(tmp.exists());
if (targetFile.leafName == "Inbox") {
let curContents = cur.directoryEntries;
let curContentsCount = 0;
while (curContents.hasMoreElements()) {
let curContent = curContents.getNext();
curContentsCount++;
}
Assert.equal(curContentsCount, 1000);
}
}
}
}
function run_test() {
localAccountUtils.loadLocalMailAccount();
// Set up two deferred pop accounts.
gServer1 = MailServices.accounts.createIncomingServer("test1", "localhost1",
"pop3");
gServer2 = MailServices.accounts.createIncomingServer("test2", "localhost2",
"pop3");
var accountPop1 = MailServices.accounts.createAccount();
var accountPop2 = MailServices.accounts.createAccount();
// Set incoming servers.
accountPop1.incomingServer = gServer1;
gServer1.QueryInterface(Ci.nsIPop3IncomingServer);
gServer1.valid = true;
accountPop2.incomingServer = gServer2;
gServer2.QueryInterface(Ci.nsIPop3IncomingServer);
gServer2.valid = true;
// Defer accounts to Local Folders.
gServer1.deferredToAccount = localAccountUtils.msgAccount.key;
gServer2.deferredToAccount = localAccountUtils.msgAccount.key;
// 'gServer1' should be deferred. Get the path of the root folder to which
// other accounts are deferred.
ok(gServer1.rootFolder.filePath.path !=
gServer1.rootMsgFolder.filePath.path);
let deferredToRootFolder = gServer1.rootMsgFolder.filePath.path;
// Account to which other accounts have been deferred.
let deferredToAccount;
// String to hold names of accounts to convert.
let accountsToConvert = "";
let accounts = allAccountsSorted(true);
for (let account of accounts) {
if (account.incomingServer.rootFolder.filePath.path ==
deferredToRootFolder) {
// Other accounts may be deferred to this account.
deferredToAccount = account;
} else if (account.incomingServer.rootMsgFolder.filePath.path ==
deferredToRootFolder) {
// This is a deferred account.
accountsToConvert += account.incomingServer.username + ", ";
}
}
accountsToConvert = accountsToConvert +
deferredToAccount.incomingServer.username;
log.info(accountsToConvert + " will be converted");
gInbox = localAccountUtils.inboxFolder;
gServer = deferredToAccount.incomingServer;
run_next_test();
}
add_task(function* setupMessages() {
let msgFile = do_get_file("../../../data/bugmail10");
// Add 1000 messages to the "Inbox" folder.
for (let i = 0; i < 1000; i++) {
yield copyFileMessage(msgFile, gInbox, false);
}
});
add_task(function testMaildirConversion() {
let mailstoreContractId = Services.prefs.getCharPref(
"mail.server." + gServer.key + ".storeContractID");
do_test_pending();
let pConverted = convertMailStoreTo(mailstoreContractId, gServer,
new EventTarget());
let originalRootFolder = gServer.rootFolder.filePath;
pConverted.then(function(val) {
log.debug("Conversion done: " + originalRootFolder.path + " => " + val);
let newRootFolder = gServer.rootFolder.filePath;
checkConversion(originalRootFolder, newRootFolder);
do_test_finished();
}).catch (function(reason) {
log.error("Conversion failed: " + reason.error);
ok(false); //Fail the test!
});
});

Просмотреть файл

@ -0,0 +1,133 @@
/* 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/. */
ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");
ChromeUtils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
ChromeUtils.import("resource:///modules/mailstoreConverter.jsm");
ChromeUtils.import("resource://gre/modules/Log.jsm");
var log = Log.repository.getLogger("MailStoreConverter");
Services.prefs.setCharPref("mail.serverDefaultStoreContractID",
"@mozilla.org/msgstore/berkeleystore;1");
var gMsgHdrs = [];
// {nsIMsgLocalMailFolder} folder carrying messages for the pop server.
var gInbox;
// {nsIMsgAccount} Account to convert.
var gAccount;
// Server for the account to convert.
var gServer;
var copyListenerWrap = {
SetMessageKey: function(aKey) {
let hdr = gInbox.GetMessageHeader(aKey);
gMsgHdrs.push({hdr: hdr, ID: hdr.messageId});
},
OnStopCopy: function(aStatus) {
// Check: message successfully copied.
Assert.equal(aStatus, 0);
}
};
var EventTarget = function () {
this.dispatchEvent = function(aEvent) {
if (aEvent.type == "progress") {
log.trace("Progress: " + aEvent.detail);
}
};
};
function copyFileMessage(aFile, aDestFolder, aIsDraftOrTemplate)
{
let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
MailServices.copy.CopyFileMessage(aFile, aDestFolder, null, aIsDraftOrTemplate,
0, "", listener, null);
return listener.promise;
}
/**
* Check that conversion worked for the given source.
* @param aSource - mbox source directory
* @param aTarget - maildir target directory
*/
function checkConversion(aSource, aTarget) {
let sourceContents = aSource.directoryEntries;
while (sourceContents.hasMoreElements()) {
let sourceContent = sourceContents.getNext().QueryInterface(Ci.nsIFile);
let sourceContentName = sourceContent.leafName;
let ext = sourceContentName.substr(-4);
let targetFile = FileUtils.File(OS.Path.join(aTarget.path,sourceContentName));
log.debug("Checking path: " + targetFile.path);
if (ext == ".dat") {
Assert.ok(targetFile.exists());
} else if (sourceContent.isDirectory()) {
Assert.ok(targetFile.exists());
checkConversion(sourceContent, targetFile);
} else if (ext != ".msf") {
Assert.ok(targetFile.exists());
let cur = FileUtils.File(OS.Path.join(targetFile.path,"cur"));
Assert.ok(cur.exists());
let tmp = FileUtils.File(OS.Path.join(targetFile.path,"tmp"));
Assert.ok(tmp.exists());
if (targetFile.leafName == "Inbox") {
let curContents = cur.directoryEntries;
let curContentsCount = 0;
while (curContents.hasMoreElements()) {
let curContent = curContents.getNext();
curContentsCount++;
}
// We had 1000 msgs in the old folder. We should have that after
// conversion too.
Assert.equal(curContentsCount, 1000);
}
}
}
}
function run_test() {
localAccountUtils.loadLocalMailAccount();
// {nsIMsgIncomingServer} pop server for the test.
gServer = MailServices.accounts.createIncomingServer("test","localhost",
"pop3");
gAccount = MailServices.accounts.createAccount();
gAccount.incomingServer = gServer;
gServer.QueryInterface(Ci.nsIPop3IncomingServer);
gServer.valid = true;
gInbox = gAccount.incomingServer.rootFolder
.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
run_next_test();
}
add_task(function* setupMessages() {
let msgFile = do_get_file("../../../data/bugmail10");
// Add 1000 messages to the "Inbox" folder.
for (let i = 0; i < 1000; i++) {
yield copyFileMessage(msgFile, gInbox, false);
}
});
add_task(function testMaildirConversion() {
let mailstoreContractId = Services.prefs.getCharPref(
"mail.server." + gServer.key + ".storeContractID");
do_test_pending();
let pConverted = convertMailStoreTo(mailstoreContractId, gServer,
new EventTarget());
let originalRootFolder = gServer.rootFolder.filePath;
pConverted.then(function(aVal) {
log.debug("Conversion done: " + originalRootFolder.path + " => " + aVal);
let newRootFolder = gServer.rootFolder.filePath;
checkConversion(originalRootFolder, newRootFolder);
do_test_finished();
}).catch(function(aReason) {
log.error("Conversion Failed: " + aReason.error);
ok(false); // Fail the test!
});
});

Просмотреть файл

@ -20,6 +20,8 @@ support-files = nodelist_test.xml data/*
[test_bug514945.js]
[test_compactFailure.js]
[test_compactColumnSave.js]
[test_mailstoreConverter.js]
[test_converterDeferredAccount.js]
[test_copyChaining.js]
[test_copyThenMoveManual.js]
[test_copyToInvalidDB.js]

Просмотреть файл

@ -0,0 +1,272 @@
/* 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/. */
self.importScripts("resource://gre/modules/osfile.jsm");
self.addEventListener("message", function(e) {
try {
// {String} sourceFile - path to file or directory encountered.
var sourceFile = e.data[1];
// {String} dest - path to directory in which the new files or directories
// need to be created.
var dest = e.data[0];
// {String} destFile - name of the file or directory encountered.
var destFile = e.data[2];
var mailstoreContractId = e.data[3];
var tmpDir = e.data[4];
var serverType = e.data[5];
var stat = OS.File.stat(sourceFile);
const constNoOfBytes = 10000000;
if (mailstoreContractId == "@mozilla.org/msgstore/maildirstore;1" &&
stat.isDir && sourceFile.substr(-4) != ".sbd") {
// Mailstore type is maildir and a maildir msg folder is encountered.
// Create a directory with path 'dest'.
OS.File.makeDir(dest, {from: tmpDir});
// If the file with path 'dest/destFile' does not exist, create it,
// open it for writing. This is the mbox msg file with the same name as
// 'sourceFile'.
let mboxFile;
if (!OS.File.exists(OS.Path.join(dest,destFile))) {
mboxFile = OS.File.open(OS.Path.join(dest,destFile), {write: true,
create: true}, {});
}
// If length of 'e.data' is greater than 6, we know that e.data carries
// maildir msg file names.
if (e.data.length > 6) {
for(let msgCount = 0; msgCount < e.data.length - 6; msgCount++) {
let n = e.data[msgCount + 6];
// Open the file 'sourceFile/cur/msgFile' for reading.
let msgFileOpen = OS.File.open(OS.Path.join(sourceFile, "cur", n));
mboxFile.write(msgFileOpen.read());
msgFileOpen.close();
// Send a message to "mailstoreConverter.jsm" indicating that a
// msg was copied. This would indicate "progress" for both imap and
// pop accounts if mailstore type is maildir and the no. of
// msgs in the account is greater than zero.
self.postMessage(["copied", OS.Path.join(sourceFile, "cur", n)]);
}
}
mboxFile.close();
// Send a message to "mailstoreConverter.jsm" indicating that an mbox msg
// file was created. This would indicate "progress" for both imap and pop
// accounts if mailstore type is maildir and the no. of messages in
// the account is 0.
self.postMessage(["file", sourceFile, e.data.length]);
} else if (stat.isDir) {
// A directory is encountered. This is a ".sbd" directory.
// Create a directory with path 'dest'.
OS.File.makeDir(dest, {from: tmpDir});
// Create a directory with same name as 'sourceFile' in the
// directory whose path is in 'dest'.
OS.File.makeDir(OS.Path.join(dest, destFile), {from: tmpDir});
// Send message to "mailstoreConverter.jsm" indicating that a directory
// was created.
// This would indicate "progress" for an imap account but not for a pop
// account if the number of messages in the pop account is more than 0 and
// mailstore type is mbox.
// This would indicate "progress" for a pop account if the number of
// messages in the pop account is 0 and the mailstore type is
// mbox.
// This would indicate "progress" for pop or imap account if the noumber
// of messages in the account is 0.
self.postMessage(["dir", sourceFile]);
} else {
// If a file is encountered, then if it is a .dat file, copy the
// file to the directory whose path is in 'dest'.
// For Local Folders, pop3, and movemail accounts, when the .msf files
// are copied, something goes wrong with the .msf files and the messages
// don't show up. Thunderbird automatically creates .msf files. So to
// resolve this, .msf files are not copied for Local Folders, pop3 and
// movemail accounts.
let ext = sourceFile.substr(-4);
if ((ext == ".msf") || (ext == ".dat")) {
if (ext == ".dat" || (serverType == "imap" || serverType == "nntp")) {
// If the directory with path 'dest' does not exist, create it.
if (!OS.File.exists(dest)) {
OS.File.makeDir(dest, {from: tmpDir});
}
OS.File.copy(sourceFile, OS.Path.join(dest,destFile));
}
// Send a message to "mailstoreConverter.jsm" indicating that a .msf or
// .dat file was copied.
// This is used to indicate progress on IMAP accounts if mailstore
// type is mbox.
// This is used to indicate progress on pop accounts if the no. of msgs
// in the account is 0 and mailstore type is mbox.
// This is used to indicate progress on pop and imap accounts if the
// no. of msgs in the account is 0 and mailstore type is maildir.
self.postMessage(["msfdat", sourceFile]);
} else if (mailstoreContractId != "@mozilla.org/msgstore/maildirstore;1") {
// An mbox message file is encountered.
// Create a directory with path 'dest'.
OS.File.makeDir(dest, {from: tmpDir});
// Create a directory with same name as the file encountered in the
// directory with path 'dest'.
// In this directory create a directory with name "cur" and a directory
// with name "tmp".
OS.File.makeDir(OS.Path.join(dest, destFile));
OS.File.makeDir(OS.Path.join(dest, destFile, "cur"));
OS.File.makeDir(OS.Path.join(dest, destFile, "tmp"));
let decoder = new TextDecoder();
let encoder = new TextEncoder();
// File to which the message is to be copied.
let targetFile = null;
// Get a timestamp for file name.
let name = Date.now();
// No. of bytes to be read from the source file.
// Needs to be a large size to read in chunks.
let noOfBytes = constNoOfBytes;
// 'text' holds the string that was read.
let text = null;
// Index of last match in 'text'.
let lastMatchIndex;
// Current position in the source file before reading bytes from it.
let position;
// New position in the source file after reading bytes from it.
let nextPos;
// New length of the text read from source file.
let nextLen;
// Position in the file after reading the bytes in the previous
// iteration.
let prevPos = 0;
// Length of the text read from source file in the previous
// iteration.
let prevLen = 0;
// Bytes read from the source file are decoded into a string and
// assigned to 'textNew'.
let textNew;
// Read the file. Since the files can be large, we read it in chunks.
let sourceFileOpen = OS.File.open(sourceFile);
while (true) {
position = sourceFileOpen.getPosition();
let array = sourceFileOpen.read(noOfBytes);
textNew = decoder.decode(array);
nextPos = sourceFileOpen.getPosition();
nextLen = textNew.length;
if (nextPos == prevPos && nextLen == prevLen) {
// Reached the last message in the source file.
if (text !== null) {
// Array to hold indices of "From -" matches found within 'text'.
let lastPos = [];
// Regular expression to find "From - " at beginning of lines.
let regexpLast = /^(From - )/gm;
let resultLast = regexpLast.exec(text);
while (resultLast !== null) {
lastPos[lastPos.length] = resultLast.index;
resultLast = regexpLast.exec(text);
}
// Create a maildir message file in 'dest/destFile/cur/'
// and open it for writing.
targetFile = OS.File.open(OS.Path.join(dest, destFile, "cur",
name.toString()), {write: true, create: true}, {});
// Extract the text in 'text' between 'lastPos[0]' ie the
// index of the first "From - " match and the end of 'text'.
targetFile.write(encoder.encode(text.substring(lastPos[0],
text.length)));
targetFile.close();
// Send a message indicating that a message was copied.
// This indicates progress for a pop account if the no. of msgs
// in the account is more than 0 and mailstore type is mbox.
self.postMessage(["copied", name, position]);
}
break;
} else {
// We might have more messages in the source file.
prevPos = nextPos;
prevLen = nextLen;
text = textNew;
}
// Array to hold indices of "From -" matches found within 'text'.
let msgPos = [];
// Regular expression to find "From - " at beginning of lines.
let regexp = /^(From - )/gm;
let result = regexp.exec(text);
while (result !== null) {
msgPos[msgPos.length] = result.index;
result = regexp.exec(text);
}
if (msgPos.length > 1) {
// More than one "From - " match is found.
noOfBytes = constNoOfBytes;
for (let i = 0; i < msgPos.length - 1; i++) {
// Create and open a new file in 'dest/destFile/cur'
// to hold the next mail.
targetFile = OS.File.open(OS.Path.join(dest, destFile, "cur",
name.toString()), {write: true, create: true}, {});
// Extract the text lying between consecutive indices, encode
// it and write it.
targetFile.write(encoder.encode(text.substring(msgPos[i],
msgPos[i + 1])));
targetFile.close();
// Send a message indicating that a mail was copied.
// This indicates progress for a pop account if the no. of msgs
// in the account is more than 0 and mailstore type is mbox.
self.postMessage(["copied", name, position + msgPos[i],
position + msgPos[i + 1]]);
// Increment 'name' to get a new file name.
// Cannot use Date.now() because it is possible to get the
// same timestamp as before.
name++;
// Set index of the (i+1)th "From - " match found in 'text'.
lastMatchIndex = msgPos[i + 1];
}
// Now 'lastMatchIndex' holds the index of the last match found in
// 'text'. So we move the position in the file to 'position +
// lastMatchIndex' from the beginning of the file.
// This ensures that the next 'text' starts from "From - "
// and that there is at least 1 match everytime.
sourceFileOpen.setPosition(position + lastMatchIndex,
OS.File.POS_START);
} else {
// If 1 match is found increase the no. of bytes to be extracted by
// 1000000 and move the position in the file to 'position', i.e. the
// position in the file before reading the bytes.
sourceFileOpen.setPosition(position, OS.File.POS_START);
noOfBytes = noOfBytes + 1000000;
}
}
// Send a message indicating that a message file was encountered.
// This indicates progress for an imap account if mailstore type is
// mbox.
// This indicates progress for a pop account if mailstore type is
// mbox and the no. of msgs in the account is 0.
self.postMessage(["file", sourceFile, textNew.length]);
}
}
} catch (e) {
// We try-catch the error because otherwise the error from File.OS is
// not properly propagated back to the worker error handling.
throw new Error(e);
}
});

Просмотреть файл

@ -0,0 +1,512 @@
/* 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/. */
var EXPORTED_SYMBOLS = ["convertMailStoreTo", "terminateWorkers"];
ChromeUtils.import("resource:///modules/iteratorUtils.jsm");
ChromeUtils.import("resource:///modules/MailUtils.js");
ChromeUtils.import("resource:///modules/mailServices.js");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");
ChromeUtils.import("resource://gre/modules/Log.jsm");
var log = Log.repository.getLogger("MailStoreConverter");
log.level = Log.Level.Debug;
log.addAppender(new Log.DumpAppender(new Log.BasicFormatter()));
// Array to hold workers.
var gConverterWorkerArray = [];
/**
* Creates "Converter" folder in tmp dir, moves the folder hierarchy of the
* account root folder creating the same folder hierarchy in Converter
* folder in tmp dir, copies the .msf and .dat files to proper places in
* Converter folder, parses the mbox files and creates corresponding folders
* and maildir files in proper places in "Converter" folder and returns a
* promise.
* @param aMailstoreContractId - account mailstore contract id
* @param {nsIMsgIncominserver} aServer - server for the account
* @param aEventTarget - target on which the "progress"
* event will be dispatched
* @return {Promise} - new root folder path of the converted
* server.
*/
function convertMailStoreTo(aMailstoreContractId, aServer, aEventTarget) {
// {nsIMsgfolder} account root folder.
var accountRootFolder = aServer.rootFolder.filePath;
// {nsIFile} tmp dir.
var tmpDir = FileUtils.getDir("TmpD", [], false);
// Array to hold path to the Converter folder in tmp dir.
var pathArray;
// No. of messages that have been copied in case of a pop account, movemail
// account, Local Folders account or any account with maildir mailstore type
// having at least 1 message.
// No. of files and folders that have been copied in case of a pop account,
// movemail account, Local Folders account or any account with maildir
// mailstore type having 0 messages.
// No. of files and folders that have been copied in case of an imap account
// or an nntp account.
var progressValue = 0;
// No. of files and folders in original account root folder for imap account
// if mailstore type is mbox, or an nntp account.
// No. of files and folders in original account root folder for a pop
// account, Local Folders account or movemail account if no. of msgs is 0
// and mailstore type is mbox.
// No. of files and folders in any non nntp account if no. of msgs is
// 0 and mailstore type is maildir.
// No. of messages in a pop account, Local Folders account or movemail
// account if no. of msgs is more than 0 and mailstore type is mbox.
// No. of messages in any non nntp account if no. of msgs is more than 0 and
// mailstore type is maildir.
var count = 0;
// If there are zero msgs in the account "zeroMessages" is true else it is
// false.
var zeroMessages = false;
// No. of files and folders in original account root folder for imap account.
// We use a progress bar to show the status of the conversion process.
// So, we need a value as the maximum value of the progress bar to measure the
// progress.
// During the conversion there are three kinds of files or folders that can be
// encountered.
// 1. A directory - This simply requires a directory to be created in the right
// place. So this a single step.
// 2. A .msf or a .dat file - This simply requires the file to be copied to the
// right place. This too is a single step.
// 3. A message file - A message file contains several messages and each one
// needs to be copied to a separate file in the right
// place. So parsing a parsing a message file consists of
// several steps.
//
// So, it's the parsing of message files that is actually time consuming and
// dealing with a directory, .msf, .dat file takes very little time.
//
// So it makes more sense to measure progress as the no. of messages copied.
// But for an imap account, getTotalMessages(true) does not give the no. of
// messages actually present in the account root folder, but gives the no. of
// messages shown on Thunderbird which is less than the no. of messages
// actually present in the account root folder. So can't use that.
//
// But we still need a way to measure progress for an imap account.
// So we measure progress by the total no. of files and folders in the account
// root folder and we increment the value of the progress bar every time a
// .msf, .dat, or a message file or a directory is encountered during
// conversion.
/**
* Count no. of files and folders in the account root folder for imap
* accounts.
* @param {nsIMsgFolder} aFolder - account root folder.
*/
var countImapFileFolders = function(aFolder) {
var count = 0;
var contents = aFolder.directoryEntries;
while (contents.hasMoreElements()) {
var content = contents.getNext()
.QueryInterface(Ci.nsIFile);
if (content.isDirectory()) {
count = count + 1 + countImapFileFolders(content);
} else {
count++;
}
}
return count;
}
/**
* Count the no. of msgs in account root folder if the mailstore type is
* maildir.
* @param {nsIMsgFolder} aFolder - account root folder.
*/
var countMaildirMsgs = function(aFolder) {
var count = 0;
var contents = aFolder.directoryEntries;
while (contents.hasMoreElements()) {
var content = contents.getNext().QueryInterface(Ci.nsIFile);
if (content.isDirectory() && content.leafName.substr(-4) != ".sbd") {
var cur = FileUtils.File(OS.Path.join(content.path,"cur"));
var curContents = cur.directoryEntries;
while (curContents.hasMoreElements()) {
curContents.getNext();
count++;
}
} else if (content.isDirectory()) {
count = count + countMaildirMsgs(content);
}
}
return count;
}
/**
* Count the no. of files and folders in account root folder if the mailstore
* type is maildir and the no. of msgs in the account is 0.
* @param {nsIMsgFolder} aFolder - account root folder.
*/
var countMaildirZeroMsgs = function(aFolder) {
var count = 0;
var contents = aFolder.directoryEntries;
while (contents.hasMoreElements()) {
var content = contents.getNext().QueryInterface(Ci.nsIFile);
// Count maildir msg folders as 1 and don't count "cur" and "tmp"
// folders.
if ((content.isDirectory() && content.leafName.substr(-4) != ".sbd") ||
!content.isDirectory()) {
count++;
} else if (content.isDirectory()) {
count = count + 1 + countMaildirMsgs(content);
}
}
return count;
}
var isMaildir = (aMailstoreContractId == "@mozilla.org/msgstore/maildirstore;1");
var conversionOk; // Resolve callback function.
var conversionFailed; // Reject callback function.
/**
* Moves the folder hierarchy of the account root folder creating the same
* folder hierarchy in Converter folder in tmp dir, copies the .msf
* and .dat files to proper places in Converter folder, parses the mbox
* files and creates corresponding folders and maildir files in proper
* places in "Converter" folder and resolves the promise returned by
* convertmailStoreTo().
*
* @param {nsIFile} aFolder - account root folder. Folder from where the
* files and directories are to be migrated.
* @param {nsIFile} aDestPath - "Converter" folder. Folder into which the
* files directories are to be migrated.
*/
var subDir = function(aFolder, aDestPath) {
let contents = aFolder.directoryEntries;
while (contents.hasMoreElements()) {
let content = contents.getNext()
.QueryInterface(Ci.nsIFile);
let converterWorker = new ChromeWorker(
"resource:///modules/converterWorker.js");
gConverterWorkerArray.push(converterWorker);
log.debug("Processing " + content.path + " => : " + aDestPath.path +
" - "+ count + "items");
// Data to be passed to the worker. Initially "dataArray" contains
// path of the directory in which the files and directories are to be
// migrated, path of the file or directory encountered, name of the file
// or directory encountered and the mailstore type, path of tmp dir,
// server type.
let dataArray = [
aDestPath.path,
content.path,
content.leafName,
aMailstoreContractId,
tmpDir.path,
aServer.type
];
if (content.isDirectory()) {
// If it is not a ".sbd" directory we know that it is a maildir msg
// folder.
if (content.leafName.substr(-4) != ".sbd") {
// Array to hold unsorted list of maildir msg filenames.
let dataArrayUnsorted = [];
// "cur" directory inside the maildir msg folder.
let cur = FileUtils.File(OS.Path.join(content.path,"cur"));
// Msg files inside "cur" directory.
let msgs = cur.directoryEntries;
while (msgs.hasMoreElements()) {
// Add filenames as integers into 'dataArrayUnsorted'.
let msg = msgs.getNext()
.QueryInterface(Ci.nsIFile);
dataArrayUnsorted.push(parseInt(msg.leafName));
}
dataArrayUnsorted.sort()
// Add the maildir msg filenames into 'dataArray' in a sorted order.
for (let elem of dataArrayUnsorted) {
dataArray.push(elem.toString());
}
}
}
// Send 'dataArray' to the worker.
converterWorker.postMessage(dataArray);
converterWorker.addEventListener("message", function(e) {
var responseWorker = e.data[0];
log.debug("Type of file or folder encountered: " + e.data);
// Dispatch the "progress" event on the event target and increment
// "progressValue" every time.
//
// mbox:
// - IMAP: a file or folder is copied.
// This is because we cannot get the no. of messages actually present
// in an imap account so we need some other way to measure the
// progress.
// - POP: a msg is copied if the no. of msgs in the account is more than
// 0. A file or folder is copied if the no. of msgs in the account is 0.
// - NNTP: a file or folder is copied.
// - MOVEMAIL: Same as POP.
// - NONE (LOCAL FOLDERS): Same as POP.
//
// maildir:
// - A msg is copied if the no. of msgs in the account is more than 0.
// - A file or folder is copied if the no. of msgs in the account is 0.
let popOrLocalOrMoveMailOrMaildir =
aServer.type == "pop3" || aServer.type == "none" ||
aServer.type == "movemail" || isMaildir;
if (((responseWorker == "copied" || (responseWorker != "copied" && zeroMessages))
&& popOrLocalOrMoveMailOrMaildir)
||
(responseWorker != "copied" && !popOrLocalOrMoveMailOrMaildir)
||
(responseWorker != "copied" && aServer.type == "nntp")
) {
progressValue++;
log.debug("Progress: " + progressValue);
let event = new Event("progress");
event.detail = parseInt((progressValue/count) * 100);
aEventTarget.dispatchEvent(event);
if (progressValue == count) {
log.info("Migration completed. Migrated " + count + " items");
// Migration is complete, get path of parent of account root
// folder into "parentPath" check if Converter folder already
// exists in "parentPath". If yes, remove it.
let lastSlash = accountRootFolder.path.lastIndexOf("/");
let parentPath = accountRootFolder.parent.path;
log.info("Path to parent folder of account root" +
" folder: " + parentPath);
let parent = new FileUtils.File(parentPath);
log.info("Path to parent folder of account root folder: " +
parent.path);
var converterFolder = new FileUtils.File(OS.Path.join(parent.path,
dir.leafName));
if (converterFolder.exists()) {
log.info("Converter folder exists in " + parentPath +
". Removing already existing folder");
converterFolder.remove(true);
}
// Move Converter folder into the parent of account root folder.
try {
dir.moveTo(parent, dir.leafName);
// {nsIFile} new account root folder.
var newRootFolder = new FileUtils.File(OS.Path.join(parent.path,
dir.leafName));
log.info("Path to new account root folder: " +
newRootFolder.path);
} catch (e) {
// Cleanup.
log.error(e);
var newRootFolder = new FileUtils.File(OS.Path.join(parent.path,
dir.leafName));
log.error("Trying to remove converter folder: " +
newRootFolder.path);
newRootFolder.remove(true);
conversionFailed(e);
}
// If the account is imap then copy the msf file for the original
// root folder and rename the copy with the name of the new root
// folder.
if (aServer.type != "pop3" && aServer.type != "none" &&
aServer.type != "movemail") {
let converterFolderMsf = new FileUtils.File(OS.Path.join(
parent.path,dir.leafName + ".msf"));
if (converterFolderMsf.exists()) {
converterFolderMsf.remove(true);
}
let oldRootFolderMsf = new FileUtils.File(OS.Path.join(
parent.path,accountRootFolder.leafName + ".msf"));
if (oldRootFolderMsf.exists()) {
oldRootFolderMsf.copyTo(parent, converterFolderMsf.leafName);
}
}
if (aServer.type == "nntp") {
let converterFolderNewsrc = new FileUtils.File(OS.Path.join(
parent.path,"newsrc-" + dir.leafName));
if (converterFolderNewsrc.exists()) {
converterFolderNewsrc.remove(true);
}
let oldNewsrc = new FileUtils.File(OS.Path.join(parent.path,
"newsrc-" + accountRootFolder.leafName));
if (oldNewsrc.exists()) {
oldNewsrc.copyTo(parent, converterFolderNewsrc.leafName);
}
}
aServer.rootFolder.filePath = newRootFolder;
aServer.localPath = newRootFolder;
log.info("Path to account root folder: " +
aServer.rootFolder.filePath.path);
// Set various preferences.
let p1 = "mail.server." + aServer.key + ".directory";
let p2 = "mail.server." + aServer.key + ".directory-rel";
let p3 = "mail.server." + aServer.key + ".newsrc.file";
let p4 = "mail.server." + aServer.key + ".newsrc.file-rel";
let p5 = "mail.server." + aServer.key + ".storeContractID";
Services.prefs.setCharPref(p1, newRootFolder.path);
log.info(p1 + ": " + newRootFolder.path)
// The directory-rel pref is of the form "[ProfD]Mail/pop.gmail.com
// " (pop accounts) or "[ProfD]ImapMail/imap.gmail.com" (imap
// accounts) ie the last slash "/" is followed by the root folder
// name. So, replace the old root folder name that follows the last
// slash with the new root folder name to set the correct value of
// directory-rel pref.
let directoryRel = Services.prefs.getCharPref(p2);
lastSlash = directoryRel.lastIndexOf("/");
directoryRel = directoryRel.slice(0, lastSlash) + "/" +
newRootFolder.leafName;
Services.prefs.setCharPref(p2, directoryRel);
log.info(p2 + ": " + directoryRel);
if (aServer.type == "nntp") {
let newNewsrc = FileUtils.File(OS.Path.join(parent.path,
"newsrc-" + newRootFolder.leafName));
Services.prefs.setCharPref(p3, newNewsrc.path);
// The newsrc.file-rel pref is of the form "[ProfD]News/newsrc-
// news.mozilla.org" ie the last slash "/" is followed by the
// newsrc file name. So, replace the old newsrc file name that
// follows the last slash with the new newsrc file name to set
// the correct value of newsrc.file-rel pref.
let newsrcRel = Services.prefs.getCharPref(p4);
lastSlash = newsrcRel.lastIndexOf("/");
newsrcRel = newsrcRel.slice(0, lastSlash) + "/" +
newNewsrc.leafName;
Services.prefs.setCharPref(p4, newsrcRel);
log.info(p4 + ": " + newsrcRel);
}
Services.prefs.setCharPref(p5, isMaildir ?
"@mozilla.org/msgstore/berkeleystore;1" :
"@mozilla.org/msgstore/maildirstore;1");
Services.prefs.savePrefFile(null);
log.info("Conversion done!");
// Resolve the promise with the path of the new account root
// folder.
conversionOk(newRootFolder.path);
}
}
});
converterWorker.addEventListener("error", function(e) {
let reasonString =
"Error at " + e.filename + ":" + e.lineno + " - " + e.message;
log.error(reasonString);
terminateWorkers();
// Cleanup.
log.error("Trying to remove converter folder: " +
aDestPath.path);
aDestPath.remove(true);
conversionFailed(e.message);
});
if (content.isDirectory()) {
if (content.leafName.substr(-4) == ".sbd") {
let dirNew = new FileUtils.File(OS.Path.join(aDestPath.path,
content.leafName));
subDir(content, dirNew);
}
}
}
}
/**
* Checks if Converter folder exists in tmp dir, removes it and creates a new
* "Converter" folder.
* @param {nsIFile} aFolder - account root folder.
*/
var createTmpConverterFolder = function(aFolder) {
let tmpFolder;
switch (aMailstoreContractId) {
case "@mozilla.org/msgstore/maildirstore;1": {
if (aFolder.leafName.substr(-8) == "-maildir") {
tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
aFolder.leafName.substr(0, aFolder.leafName.length - 8) + "-mbox"));
} else {
tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
aFolder.leafName + "-mbox"));
}
if (tmpFolder.exists()) {
log.info("Temporary Converter folder " + tmpFolder.path +
"exists in tmp dir. Removing it");
tmpFolder.remove(true);
}
return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
}
case "@mozilla.org/msgstore/berkeleystore;1": {
if (aFolder.leafName.substr(-5) == "-mbox") {
tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
aFolder.leafName.substr(0, aFolder.leafName.length - 5) +
"-maildir"));
} else {
tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
aFolder.leafName + "-maildir"));
}
if (tmpFolder.exists()) {
log.info("Temporary Converter folder " + tmpFolder.path +
"exists in tmp dir. Removing it");
tmpFolder.remove(true);
}
return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
}
default: {
throw new Error("Unexpected mailstoreContractId: " +
aMailstoreContractId);
}
}
}
if (isMaildir && aServer.type != "nntp") {
count = countMaildirMsgs(accountRootFolder);
if (count == 0) {
count = countMaildirZeroMsgs(accountRootFolder);
zeroMessages = true;
}
} else if (aServer.type == "pop3" ||
aServer.type == "none" || // none: Local Folders.
aServer.type == "movemail") {
count = aServer.rootFolder.getTotalMessages(true);
if (count == 0) {
count = countImapFileFolders(accountRootFolder);
zeroMessages = true;
}
} else if (aServer.type == "imap" || aServer.type == "nntp") {
count = countImapFileFolders(accountRootFolder);
}
// Go offline before conversion, so there aren't messages coming in during
// the process.
Services.io.offline = true;
let dir = createTmpConverterFolder(accountRootFolder);
return new Promise(function(resolve, reject) {
conversionOk = resolve;
conversionFailed = reject;
subDir(accountRootFolder, dir);
});
}
/**
* Terminate all workers.
*/
function terminateWorkers() {
for (let worker of gConverterWorkerArray) {
worker.terminate();
}
}

Просмотреть файл

@ -49,6 +49,7 @@ SOURCES += [
EXTRA_JS_MODULES += [
'ABQueryUtils.jsm',
'converterWorker.js',
'errUtils.js',
'folderUtils.jsm',
'hostnameUtils.jsm',
@ -58,6 +59,7 @@ EXTRA_JS_MODULES += [
'JXON.js',
'mailnewsMigrator.js',
'mailServices.js',
'mailstoreConverter.jsm',
'msgDBCacheManager.js',
'OAuth2.jsm',
'OAuth2Providers.jsm',

Просмотреть файл

@ -0,0 +1,151 @@
/* 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/. */
ChromeUtils.import("resource://gre/modules/osfile.jsm");
ChromeUtils.import("resource:///modules/mailstoreConverter.jsm");
ChromeUtils.import("resource://gre/modules/Log.jsm");
Services.prefs.setCharPref("mail.serverDefaultStoreContractID",
"@mozilla.org/msgstore/berkeleystore;1");
load("../../../resources/logHelper.js");
load("../../../resources/asyncTestUtils.js");
load("../../../resources/messageGenerator.js");
load("../../../resources/alertTestUtils.js");
var log = Log.repository.getLogger("MailStoreConverter");
var FileUtils = ChromeUtils.import("resource://gre/modules/FileUtils.jsm")
.FileUtils;
// Globals
var gRootFolder;
var gMsgFile1 = do_get_file("../../../data/bugmail10");
// Copied straight from the example files
var gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
var gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
var gMsgId3 = "4849BF7B.2030800@example.com";
var gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org";
var gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org";
function checkConversion(aSource, aTarget) {
let sourceContents = aSource.directoryEntries;
while (sourceContents.hasMoreElements()) {
let sourceContent = sourceContents.getNext().QueryInterface(Ci.nsIFile);
let sourceContentName = sourceContent.leafName;
let ext = sourceContentName.slice(-4);
let targetFile = FileUtils.File(OS.Path.join(aTarget.path,sourceContentName));
log.debug("Checking path: " + targetFile.path);
if (ext == ".msf" || ext == ".dat") {
Assert.ok(targetFile.exists());
} else if (sourceContent.isDirectory()) {
Assert.ok(targetFile.exists());
checkConversion(sourceContent, targetFile);
} else {
Assert.ok(targetFile.exists());
let cur = FileUtils.File(OS.Path.join(targetFile.path,"cur"));
Assert.ok(cur.exists());
let tmp = FileUtils.File(OS.Path.join(targetFile.path,"tmp"));
Assert.ok(tmp.exists());
if (targetFile.leafName == "INBOX") {
let curContents = cur.directoryEntries;
let curContentsCount = 0;
while (curContents.hasMoreElements()) {
let curContent = curContents.getNext();
curContentsCount += 1;
}
Assert.equal(curContentsCount, 8);
}
}
}
}
var EventTarget = function () {
this.dispatchEvent = function (aEvent) {
if (aEvent.type == "progress") {
log.trace("Progress: " + aEvent.detail);
}
};
};
// Adds some messages directly to a mailbox (eg new mail).
function addMessagesToServer(aMessages, aMailbox)
{
// For every message we have, we need to convert it to a file:/// URI
aMessages.forEach(function (message)
{
let URI =
Services.io.newFileURI(message.file).QueryInterface(Ci.nsIFileURL);
message.spec = URI.spec;
});
// Create the imapMessages and store them on the mailbox.
aMessages.forEach(function (message)
{
aMailbox.addMessage(new imapMessage(message.spec, aMailbox.uidnext++, []));
});
}
function setup() {
setupIMAPPump();
gRootFolder = IMAPPump.incomingServer.rootFolder;
// These hacks are required because we've created the inbox before
// running initial folder discovery, and adding the folder bails
// out before we set it as verified online, so we bail out, and
// then remove the INBOX folder since it's not verified.
IMAPPump.inbox.hierarchyDelimiter = '/';
IMAPPump.inbox.verifiedAsOnlineFolder = true;
// Add a couple of messages to the INBOX.
// This is synchronous.
addMessagesToServer([{file: gMsgFile1, messageId: gMsgId1},
{file: gMsgFile1, messageId: gMsgId4},
{file: gMsgFile1, messageId: gMsgId2},
{file: gMsgFile1, messageId: gMsgId5}],
IMAPPump.daemon.getMailbox("INBOX"), IMAPPump.inbox);
}
function run_test() {
setup();
run_next_test();
}
add_task(function* downloadForOffline() {
// Download for offline use.
dump("Downloading for offline use\n");
let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
IMAPPump.inbox.downloadAllForOffline(promiseUrlListener, null);
yield promiseUrlListener.promise;
});
add_task(function convert() {
let mailstoreContractId = Services.prefs.getCharPref(
"mail.server." + IMAPPump.incomingServer.key + ".storeContractID");
let eventTarget = new EventTarget();
let pConverted = convertMailStoreTo(mailstoreContractId,
IMAPPump.incomingServer, eventTarget);
do_test_pending();
let originalRootFolder = IMAPPump.incomingServer.rootFolder.filePath;
pConverted.then(function(val) {
log.debug("Conversion done: " + originalRootFolder.path + " => " + val);
let newRootFolder = IMAPPump.incomingServer.rootFolder.filePath;
checkConversion(originalRootFolder, newRootFolder);
let newRootFolderMsf = FileUtils.File(newRootFolder.path + ".msf");
Assert.ok(newRootFolderMsf.exists());
do_test_finished();
}).catch (function(reason) {
log.error("Conversion Failed: " + reason.error);
do_test_finished();
});
});
add_task(function teardown() {
gRootFolder = null;
teardownIMAPPump();
});

Просмотреть файл

@ -7,6 +7,7 @@ tail =
[test_bug460636.js]
[test_chunkLastLF.js]
[test_compactOfflineStore.js]
[test_converterImap.js]
[test_copyThenMove.js]
[test_customCommandReturnsFetchResponse.js]
[test_dod.js]

Просмотреть файл

@ -65,6 +65,8 @@ messenger.jar:
content/messenger/SmtpServerEdit.js (base/prefs/content/SmtpServerEdit.js)
content/messenger/smtpEditOverlay.xul (base/prefs/content/smtpEditOverlay.xul)
content/messenger/smtpEditOverlay.js (base/prefs/content/smtpEditOverlay.js)
content/messenger/converterDialog.xhtml (base/prefs/content/converterDialog.xhtml)
content/messenger/converterDialog.js (base/prefs/content/converterDialog.js)
content/messenger/removeAccount.xul (base/prefs/content/removeAccount.xul)
content/messenger/removeAccount.js (base/prefs/content/removeAccount.js)
content/messenger/msgSelectOfflineFolders.xul (base/content/msgSelectOfflineFolders.xul)

Просмотреть файл

@ -536,9 +536,16 @@ pref("mail.server.default.offline_download",true);
// -1 means no limit, no purging of offline stores.
pref("mail.server.default.autosync_max_age_days", -1);
// can we change the store type?
// Can we change the store type without conversion? (=has the store been used)
pref("mail.server.default.canChangeStoreType", false);
// Store conversion (mbox <-> maildir)
#ifndef RELEASE_OR_BETA
pref("mail.store_conversion_enabled", true);
#else
pref("mail.store_conversion_enabled", false);
#endif
// This is the default store contractID for newly created servers.
// We don't use mail.server.default because we want to ensure that the
// store contract id is always written out to prefs.js