1 // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 var Mail = Module("mail", {
8 init: function init() {
9 // used for asynchronously selecting messages after wrapping folders
10 this._selectMessageKeys = [];
11 this._selectMessageCount = 1;
12 this._selectMessageReverse = false;
14 this._mailSession = Cc["@mozilla.org/messenger/services/session;1"].getService(Ci.nsIMsgMailSession);
15 this._notifyFlags = Ci.nsIFolderListener.intPropertyChanged | Ci.nsIFolderListener.event;
16 this._mailSession.AddFolderListener(this._folderListener, this._notifyFlags);
20 OnItemAdded: function (parentItem, item) {},
21 OnItemRemoved: function (parentItem, item) {},
22 OnItemPropertyChanged: function (item, property, oldValue, newValue) {},
23 OnItemIntPropertyChanged: function (item, property, oldValue, newValue) {},
24 OnItemBoolPropertyChanged: function (item, property, oldValue, newValue) {},
25 OnItemUnicharPropertyChanged: function (item, property, oldValue, newValue) {},
26 OnItemPropertyFlagChanged: function (item, property, oldFlag, newFlag) {},
28 OnItemEvent: function (folder, event) {
29 let eventType = event.toString();
30 if (eventType == "FolderLoaded") {
32 let msgFolder = folder.QueryInterface(Ci.nsIMsgFolder);
33 autocommands.trigger("FolderLoaded", { url: msgFolder });
35 // Jump to a message when requested
37 if (mail._selectMessageKeys.length > 0) {
38 for (let j = 0; j < mail._selectMessageKeys.length; j++)
39 indices.push([gDBView.findIndexFromKey(mail._selectMessageKeys[j], true), mail._selectMessageKeys[j]]);
42 let index = mail._selectMessageCount - 1;
43 if (mail._selectMessageReverse)
44 index = mail._selectMessageKeys.length - 1 - index;
46 gDBView.selectMsgByKey(indices[index][1]);
47 mail._selectMessageKeys = [];
51 /*else if (eventType == "ImapHdrDownloaded") {}
52 else if (eventType == "DeleteOrMoveMsgCompleted") {}
53 else if (eventType == "DeleteOrMoveMsgFailed") {}
54 else if (eventType == "AboutToCompact") {}
55 else if (eventType == "CompactCompleted") {}
56 else if (eventType == "RenameCompleted") {}
57 else if (eventType == "JunkStatusChanged") {}*/
61 _getCurrentFolderIndex: function () {
62 // for some reason, the index is interpreted as a string, therefore the parseInt
63 return parseInt(gFolderTreeView.getIndexOfFolder(gFolderTreeView.getSelectedFolders()[0]));
66 _getRSSUrl: function () {
67 return gDBView.hdrForFirstSelectedMessage.messageId.replace(/(#.*)?@.*$/, "");
70 _moveOrCopy: function (copy, destinationFolder, operateOnThread) {
71 let folders = mail.getFolders(destinationFolder);
72 if (folders.length == 0)
73 return void dactyl.echoerr(_("addressbook.noMatchingFolder", destinationFolder));
74 else if (folders.length > 1)
75 return dactyl.echoerr(_("addressbook.multipleFolderMatches", destinationFolder));
77 let count = gDBView.selection.count;
79 return void dactyl.beep();
81 (copy ? MsgCopyMessage : MsgMoveMessage)(folders[0]);
82 util.timeout(function () {
83 dactyl.echomsg(count + " message(s) " + (copy ? "copied" : "moved") + " to " + folders[0].prettyName, 1);
87 _parentIndex: function (index) {
89 let tree = GetThreadTree();
92 let tmp = tree.view.getParentIndex(parent);
101 // does not wrap yet, intentional?
102 _selectUnreadFolder: function (backwards, count) {
103 count = Math.max(1, count);
104 let direction = backwards ? -1 : 1;
105 let c = this._getCurrentFolderIndex();
108 while (count > 0 && (c + i) < gFolderTreeView.rowCount && (c + i) >= 0) {
109 let resource = gFolderTreeView._rowMap[c + i]._folder;
110 if (!resource.isServer && resource.getNumUnread(false)) {
116 if (!folder || count > 0)
119 gFolderTreeView.selection.timedSelect(c + folder, 500);
122 _escapeRecipient: function (recipient) {
124 recipient = recipient.replace(/"/g, "");
125 return "\"" + recipient + "\"";
128 get currentAccount() this.currentFolder.rootFolder,
130 get currentFolder() gFolderTreeView.getSelectedFolders()[0],
132 /** @property {[nsISmtpServer]} The list of configured SMTP servers. */
134 let servers = services.smtp.smtpServers;
137 while (servers.hasMoreElements()) {
138 let server = servers.getNext();
139 if (server instanceof Ci.nsISmtpServer)
146 composeNewMail: function (args) {
147 let params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams);
148 params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields);
151 if (args.originalMsg)
152 params.originalMsgURI = args.originalMsg;
154 params.composeFields.to = args.to;
156 params.composeFields.cc = args.cc;
158 params.composeFields.bcc = args.bcc;
160 params.composeFields.newsgroups = args.newsgroups;
162 params.composeFields.subject = args.subject;
164 params.composeFields.body = args.body;
166 if (args.attachments) {
167 while (args.attachments.length > 0) {
168 let url = args.attachments.pop();
169 let file = io.getFile(url);
171 return void dactyl.echoerr(_("mail.cantAttachFile", url), commandline.FORCE_SINGLELINE);
173 attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment);
174 attachment.url = "file://" + file.path;
175 params.composeFields.addAttachment(attachment);
180 params.type = Ci.nsIMsgCompType.New;
182 services.compose.OpenComposeWindowWithParams(null, params);
185 // returns an array of nsIMsgFolder objects
186 getFolders: function (filter, includeServers, includeMsgFolders) {
191 filter = filter.toLowerCase();
193 if (includeServers === undefined)
194 includeServers = false;
195 if (includeMsgFolders === undefined)
196 includeMsgFolders = true;
198 for (let i = 0; i < gFolderTreeView.rowCount; i++) {
199 let resource = gFolderTreeView._rowMap[i]._folder;
200 if ((resource.isServer && !includeServers) || (!resource.isServer && !includeMsgFolders))
203 let folderString = resource.server.prettyName + ": " + resource.name;
205 if (resource.prettiestName.toLowerCase().indexOf(filter) >= 0)
206 folders.push(resource);
207 else if (folderString.toLowerCase().indexOf(filter) >= 0)
208 folders.push(resource);
213 getNewMessages: function (currentAccountOnly) {
214 if (currentAccountOnly)
215 MsgGetMessagesForAccount();
217 GetMessagesForAllAuthenticatedAccounts();
220 getStatistics: function (currentAccountOnly) {
221 let accounts = currentAccountOnly ? [this.currentAccount]
222 : this.getFolders("", true, false);
224 let unreadCount = 0, totalCount = 0, newCount = 0;
225 for (let i = 0; i < accounts.length; i++) {
226 let account = accounts[i];
227 unreadCount += account.getNumUnread(true); // true == deep (includes subfolders)
228 totalCount += account.getTotalMessages(true);
229 newCount += account.getNumUnread(true);
232 return { numUnread: unreadCount, numTotal: totalCount, numNew: newCount };
235 collapseThread: function () {
236 let tree = GetThreadTree();
238 let parent = this._parentIndex(tree.currentIndex);
239 if (tree.changeOpenState(parent, false)) {
240 tree.view.selection.select(parent);
241 tree.treeBoxObject.ensureRowIsVisible(parent);
248 expandThread: function () {
249 let tree = GetThreadTree();
251 let row = tree.currentIndex;
252 if (row >= 0 && tree.changeOpenState(row, true))
259 * General-purpose method to find messages.
261 * @param {function(nsIMsgDBHdr):boolean} validatorFunc Return
262 * true/false whether msg should be selected or not.
263 * @param {boolean} canWrap When true, wraps around folders.
264 * @param {boolean} openThreads Should we open closed threads?
265 * @param {boolean} reverse Change direction of searching.
267 selectMessage: function (validatorFunc, canWrap, openThreads, reverse, count) {
268 function currentIndex() {
269 let index = gDBView.selection.currentIndex;
275 function closedThread(index) {
276 if (!(gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay))
279 index = (typeof index == "number") ? index : currentIndex();
280 return !gDBView.isContainerOpen(index) && !gDBView.isContainerEmpty(index);
283 if (typeof validatorFunc != "function")
286 if (typeof count != "number" || count < 1)
289 // first try to find in current folder
291 for (let i = currentIndex() + (reverse ? -1 : (openThreads && closedThread() ? 0 : 1));
292 reverse ? (i >= 0) : (i < gDBView.rowCount);
293 reverse ? i-- : i++) {
294 let key = gDBView.getKeyAt(i);
295 let msg = gDBView.db.GetMsgHdrForKey(key);
298 if (openThreads && closedThread(i)) {
299 let thread = gDBView.db.GetThreadContainingMsgHdr(msg);
300 let originalCount = count;
302 for (let j = (i == currentIndex() && !reverse) ? 1 : (reverse ? thread.numChildren - 1 : 0);
303 reverse ? (j >= 0) : (j < thread.numChildren);
304 reverse ? j-- : j++) {
305 msg = thread.getChildAt(j);
306 if (validatorFunc(msg) && --count == 0) {
307 // this hack is needed to get the correct message, because getChildAt() does not
308 // necessarily return the messages in the order they are displayed
309 gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500);
310 GetThreadTree().treeBoxObject.ensureRowIsVisible(i);
312 GetThreadTree().changeOpenState(i, true);
313 this.selectMessage(validatorFunc, false, false, false, originalCount);
319 else { // simple non-threaded message
320 if (validatorFunc(msg) && --count == 0) {
321 gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500);
322 GetThreadTree().treeBoxObject.ensureRowIsVisible(i);
329 // then in other folders
331 this._selectMessageReverse = reverse;
333 let folders = this.getFolders("", true, true);
334 let ci = this._getCurrentFolderIndex();
335 for (let i = 1; i < folders.length; i++) {
336 let index = (i + ci) % folders.length;
338 index = folders.length - 1 - index;
340 let folder = folders[index];
344 this._selectMessageCount = count;
345 this._selectMessageKeys = [];
347 // sometimes folder.getMessages can fail with an exception
348 // TODO: find out why, and solve the problem
350 var msgs = folder.messages;
353 msgs = folder.getMessages(msgWindow); // for older thunderbirds
354 dactyl.dump("WARNING: " + folder.prettyName + " failed to getMessages, trying old API");
358 while (msgs.hasMoreElements()) {
359 let msg = msgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
360 if (validatorFunc(msg)) {
362 this._selectMessageKeys.push(msg.messageKey);
367 // SelectFolder is asynchronous, message is selected in this._folderListener
368 SelectFolder(folder.URI);
374 // TODO: finally for the "rest" of the current folder
379 setHTML: function (value) {
380 let values = [[true, 1, gDisallow_classes_no_html], // plaintext
381 [false, 0, 0], // HTML
382 [false, 3, gDisallow_classes_no_html]]; // sanitized/simple HTML
384 if (typeof value != "number" || value < 0 || value > 2)
387 gPrefBranch.setBoolPref("mailnews.display.prefer_plaintext", values[value][0]);
388 gPrefBranch.setIntPref("mailnews.display.html_as", values[value][1]);
389 gPrefBranch.setIntPref("mailnews.display.disallow_mime_handlers", values[value][2]);
394 commands: function initCommands(dactyl, modules, window) {
395 commands.add(["go[to]"],
398 let count = Math.max(0, args.count - 1);
399 let arg = args.literalArg || "Inbox";
401 let folder = mail.getFolders(arg, true, true)[count];
403 dactyl.echoerr(_("command.goto.folderNotExist", arg));
404 else if (dactyl.forceNewTab)
405 MsgOpenNewTabForFolder(folder.URI);
407 SelectFolder(folder.URI);
411 completer: function (context) completion.mailFolder(context),
416 commands.add(["m[ail]"],
417 "Write a new message",
420 mailargs.to = args.join(", ");
421 mailargs.subject = args["-subject"];
422 mailargs.bcc = args["-bcc"] || [];
423 mailargs.cc = args["-cc"] || [];
424 mailargs.body = args["-text"];
425 mailargs.attachments = args["-attachment"] || [];
427 let addresses = args;
429 addresses = addresses.concat(mailargs.bcc);
431 addresses = addresses.concat(mailargs.cc);
433 // TODO: is there a better way to check for validity?
434 if (addresses.some(recipient => !(/\S@\S+\.\S/.test(recipient))))
435 return void dactyl.echoerr(_("command.mail.invalidEmailAddress"));
437 mail.composeNewMail(mailargs);
440 // TODO: completers, validators - whole shebang. Do people actually use this? --djk
442 { names: ["-subject", "-s"], type: CommandOption.STRING, description: "Subject line"},
443 { names: ["-attachment", "-a"], type: CommandOption.LIST, description: "List of attachments"},
444 { names: ["-bcc", "-b"], type: CommandOption.LIST, description: "Blind Carbon Copy addresses"},
445 { names: ["-cc", "-c"], type: CommandOption.LIST, description: "Carbon Copy addresses"},
446 { names: ["-text", "-t"], type: CommandOption.STRING, description: "Message body"}
450 commands.add(["copy[to]"],
451 "Copy selected messages",
452 function (args) { mail._moveOrCopy(true, args.literalArg); },
455 completer: function (context) completion.mailFolder(context),
459 commands.add(["move[to]"],
460 "Move selected messages",
461 function (args) { mail._moveOrCopy(false, args.literalArg); },
464 completer: function (context) completion.mailFolder(context),
468 commands.add(["empty[trash]"],
469 "Empty trash of the current account",
470 function () { window.goDoCommand("cmd_emptyTrash"); },
473 commands.add(["get[messages]"],
474 "Check for new messages",
475 function (args) { mail.getNewMessages(!args.bang); },
481 completion: function initCompletion(dactyl, modules, window) {
482 completion.mailFolder = function mailFolder(context) {
483 let folders = mail.getFolders(context.filter);
484 context.anchored = false;
485 context.quote = false;
486 context.completions = folders.map(folder =>
487 [folder.server.prettyName + ": " + folder.name,
488 "Unread: " + folder.getNumUnread(false)]);
491 mappings: function initMappings(dactyl, modules, window) {
492 var myModes = config.mailModes;
494 mappings.add(myModes, ["<Return>", "i"],
495 "Inspect (focus) message",
496 function () { content.focus(); });
498 mappings.add(myModes, ["I"],
499 "Open the message in new tab",
501 if (gDBView && gDBView.selection.count < 1)
502 return void dactyl.beep();
504 MsgOpenNewTabForMessage();
507 mappings.add(myModes, ["<Space>"],
508 "Scroll message or select next unread one",
511 mappings.add(myModes, ["t"],
513 function () { gDBView.ExpandAndSelectThreadByIndex(GetThreadTree().currentIndex, false); });
515 mappings.add(myModes, ["d", "<Del>"],
516 "Move mail to Trash folder",
517 function () { window.goDoCommand("cmd_delete"); });
519 mappings.add(myModes, ["j", "<Right>"],
520 "Select next message",
521 function ({ count }) { mail.selectMessage(msg => true, false, false, false, count); },
524 mappings.add(myModes, ["gj"],
525 "Select next message, including closed threads",
526 function ({ count }) { mail.selectMessage(msg => true, false, true, false, count); },
529 mappings.add(myModes, ["J", "<Tab>"],
530 "Select next unread message",
531 function ({ count }) { mail.selectMessage(msg => !msg.isRead, true, true, false, count); },
534 mappings.add(myModes, ["k", "<Left>"],
535 "Select previous message",
536 function ({ count }) { mail.selectMessage(msg => true, false, false, true, count); },
539 mappings.add(myModes, ["gk"],
540 "Select previous message",
541 function ({ count }) { mail.selectMessage(msg => true, false, true, true, count); },
544 mappings.add(myModes, ["K"],
545 "Select previous unread message",
546 function ({ count }) { mail.selectMessage(msg => !msg.isRead, true, true, true, count); },
549 mappings.add(myModes, ["*"],
550 "Select next message from the same sender",
551 function ({ count }) {
552 let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase();
553 mail.selectMessage(msg => msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, false, count);
557 mappings.add(myModes, ["#"],
558 "Select previous message from the same sender",
559 function ({ count }) {
560 let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase();
561 mail.selectMessage(msg => msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, true, count);
566 mappings.add(myModes, ["m"],
567 "Compose a new message",
568 function () { CommandExMode().open("mail -subject="); });
570 mappings.add(myModes, ["M"],
571 "Compose a new message to the sender of selected mail",
573 let to = mail._escapeRecipient(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor);
574 CommandExMode().open("mail " + to + " -subject=");
577 mappings.add(myModes, ["r"],
579 function () { window.goDoCommand("cmd_reply"); });
581 mappings.add(myModes, ["R"],
583 function () { window.goDoCommand("cmd_replyall"); });
585 mappings.add(myModes, ["f"],
587 function () { window.goDoCommand("cmd_forward"); });
589 mappings.add(myModes, ["F"],
590 "Forward message inline",
591 function () { window.goDoCommand("cmd_forwardInline"); });
594 mappings.add(myModes, ["<Down>"],
595 "Scroll message down",
596 function ({ count }) { buffer.scrollLines(Math.max(count, 1)); },
599 mappings.add(myModes, ["<Up>"],
601 function ({ count }) { buffer.scrollLines(-Math.max(count, 1)); },
604 mappings.add([modes.MESSAGE], ["<Left>"],
605 "Select previous message",
606 function ({ count }) { mail.selectMessage(msg => true, false, false, true, count); },
609 mappings.add([modes.MESSAGE], ["<Right>"],
610 "Select next message",
611 function ({ count }) { mail.selectMessage(msg => true, false, false, false, count); },
615 mappings.add(myModes, ["u"],
618 if (messenger.canUndo())
619 messenger.undo(msgWindow);
623 mappings.add(myModes, ["<C-r>"],
626 if (messenger.canRedo())
627 messenger.redo(msgWindow);
633 mappings.add(myModes, ["gm"],
635 function () { mail.getNewMessages(); });
637 mappings.add(myModes, ["gM"],
638 "Get new messages for current account only",
639 function () { mail.getNewMessages(true); });
642 mappings.add(myModes, ["c"],
644 function () { CommandExMode().open("goto "); });
646 mappings.add(myModes, ["s"],
647 "Move selected messages",
648 function () { CommandExMode().open("moveto "); });
650 mappings.add(myModes, ["S"],
651 "Copy selected messages",
652 function () { CommandExMode().open("copyto "); });
654 mappings.add(myModes, ["<C-s>"],
656 function () { mail._moveOrCopy(false, options["archivefolder"]); });
658 mappings.add(myModes, ["]s"],
659 "Select next starred message",
660 function ({ count }) { mail.selectMessage(msg => msg.isFlagged, true, true, false, count); },
663 mappings.add(myModes, ["[s"],
664 "Select previous starred message",
665 function ({ count }) { mail.selectMessage(msg => msg.isFlagged, true, true, true, count); },
668 mappings.add(myModes, ["]a"],
669 "Select next message with an attachment",
670 function ({ count }) { mail.selectMessage(msg => gDBView.db.HasAttachments(msg.messageKey), true, true, false, count); },
673 mappings.add(myModes, ["[a"],
674 "Select previous message with an attachment",
675 function ({ count }) { mail.selectMessage(msg => gDBView.db.HasAttachments(msg.messageKey), true, true, true, count); },
679 mappings.add(myModes, ["gi"],
681 function ({ count }) {
682 let folder = mail.getFolders("Inbox", false, true)[(count > 0) ? (count - 1) : 0];
684 SelectFolder(folder.URI);
690 mappings.add(myModes, ["<C-n>"],
691 "Select next folder",
692 function ({ count }) {
693 let newPos = mail._getCurrentFolderIndex() + Math.max(1, count);
694 if (newPos >= gFolderTreeView.rowCount) {
695 newPos = newPos % gFolderTreeView.rowCount;
696 commandline.echo(_("finder.atBottom"), commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
698 gFolderTreeView.selection.timedSelect(newPos, 500);
702 mappings.add(myModes, ["<C-N>"],
703 "Go to next mailbox with unread messages",
704 function ({ count }) {
705 mail._selectUnreadFolder(false, count);
709 mappings.add(myModes, ["<C-p>"],
710 "Select previous folder",
711 function ({ count }) {
712 let newPos = mail._getCurrentFolderIndex() - Math.max(1, count);
714 newPos = (newPos % gFolderTreeView.rowCount) + gFolderTreeView.rowCount;
715 commandline.echo(_("finder.atTop"), commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
717 gFolderTreeView.selection.timedSelect(newPos, 500);
721 mappings.add(myModes, ["<C-P>"],
722 "Go to previous mailbox with unread messages",
723 function ({ count }) {
724 mail._selectUnreadFolder(true, count);
729 mappings.add(myModes, ["za"],
730 "Toggle thread collapsed/expanded",
731 function () { if (!mail.expandThread()) mail.collapseThread(); });
733 mappings.add(myModes, ["zc"],
735 function () { mail.collapseThread(); });
737 mappings.add(myModes, ["zo"],
739 function () { mail.expandThread(); });
741 mappings.add(myModes, ["zr", "zR"],
742 "Expand all threads",
743 function () { window.goDoCommand("cmd_expandAllThreads"); });
745 mappings.add(myModes, ["zm", "zM"],
746 "Collapse all threads",
747 function () { window.goDoCommand("cmd_collapseAllThreads"); });
749 mappings.add(myModes, ["<C-i>"],
751 function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.forward, true); },
754 mappings.add(myModes, ["<C-o>"],
756 function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.back, true); },
759 mappings.add(myModes, ["gg"],
760 "Select first message",
761 function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.firstMessage, true); },
764 mappings.add(myModes, ["G"],
765 "Select last message",
766 function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.lastMessage, false); },
770 mappings.add(myModes, ["l"],
773 if (!GetSelectedMessages())
774 return void dactyl.beep();
777 case "r": MsgMarkMsgAsRead(); break;
778 case "s": MsgMarkAsFlagged(); break;
779 case "i": ToggleMessageTagKey(1); break; // Important
780 case "w": ToggleMessageTagKey(2); break; // Work
781 case "p": ToggleMessageTagKey(3); break; // Personal
782 case "t": ToggleMessageTagKey(4); break; // TODO
783 case "l": ToggleMessageTagKey(5); break; // Later
784 default: dactyl.beep();
791 // TODO: change binding?
792 mappings.add(myModes, ["T"],
793 "Mark current folder as read",
795 if (mail.currentFolder.isServer)
796 return dactyl.beep();
798 mail.currentFolder.markAllMessagesRead(msgWindow);
801 mappings.add(myModes, ["<C-t>"],
802 "Mark all messages as read",
804 mail.getFolders("", false).forEach(function (folder) { folder.markAllMessagesRead(msgWindow); });
808 mappings.add(myModes, ["h"],
809 "Toggle displayed headers",
811 let value = gPrefBranch.getIntPref("mail.show_headers", 2);
812 gPrefBranch.setIntPref("mail.show_headers", value == 2 ? 1 : 2);
816 mappings.add(myModes, ["x"],
817 "Toggle HTML message display",
819 let wantHtml = (gPrefBranch.getIntPref("mailnews.display.html_as", 1) == 1);
820 mail.setHTML(wantHtml ? 1 : 0);
824 mappings.add(myModes, ["Y"],
828 let subject = gDBView.hdrForFirstSelectedMessage.mime2DecodedSubject;
829 dactyl.clipboardWrite(subject, true);
831 catch (e) { dactyl.beep(); }
834 mappings.add(myModes, ["y"],
835 "Yank sender or feed URL",
838 if (mail.currentAccount.server.type == "rss")
839 dactyl.clipboardWrite(mail._getRSSUrl(), true);
841 dactyl.clipboardWrite(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor, true);
843 catch (e) { dactyl.beep(); }
846 // RSS specific mappings
847 mappings.add(myModes, ["p"],
848 "Open RSS message in browser",
851 if (mail.currentAccount.server.type == "rss")
852 messenger.launchExternalURL(mail._getRSSUrl());
853 // TODO: what to do for non-rss message?
860 services: function initServices(dactyl, modules, window) {
861 services.add("smtp", "@mozilla.org/messengercompose/smtp;1", Ci.nsISmtpService);
862 services.add("compose", "@mozilla.org/messengercompose;1", "nsIMsgComposeService");
864 modes: function initModes(dactyl, modules, window) {
865 modes.addMode("MESSAGE", {
867 description: "Active the message is focused",
868 bases: [modes.COMMAND]
871 options: function initOptions(dactyl, modules, window) {
872 // FIXME: why does this default to "Archive", I don't have one? The default
873 // value won't validate now. mst please fix. --djk
874 options.add(["archivefolder"],
875 "Set the archive folder",
878 completer: function (context) completion.mailFolder(context)
881 // TODO: generate the possible values dynamically from the menu
882 options.add(["layout"],
883 "Set the layout of the mail window",
886 setter: function (value) {
888 case "classic": ChangeMailLayout(0); break;
889 case "wide": ChangeMailLayout(1); break;
890 case "vertical": ChangeMailLayout(2); break;
891 // case "inherit" just does nothing
896 completer: function (context) [
897 ["inherit", "Default View"], // FIXME: correct description?
898 ["classic", "Classic View"],
899 ["wide", "Wide View"],
900 ["vertical", "Vertical View"]
904 options.add(["smtpserver", "smtp"],
905 "Set the default SMTP server",
906 "string", services.smtp.defaultServer.key, // TODO: how should we handle these persistent external defaults - "inherit" or null?
908 getter: function () services.smtp.defaultServer.key,
909 setter: function (value) {
910 let server = mail.smtpServers.filter(s => s.key == value)[0];
911 services.smtp.defaultServer = server;
914 completer: function (context) [[s.key, s.serverURI] for ([, s] in Iterator(mail.smtpServers))]
917 /*options.add(["threads"],
918 "Use threading to group messages",
921 setter: function (value) {
933 // vim: set fdm=marker sw=4 sts=4 ts=8 et: