]> git.donarmstrong.com Git - dactyl.git/blob - teledactyl/content/mail.js
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / teledactyl / content / mail.js
1 // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
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;
13
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);
17     },
18
19     _folderListener: {
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) {},
27
28         OnItemEvent: function (folder, event) {
29             let eventType = event.toString();
30             if (eventType == "FolderLoaded") {
31                 if (folder) {
32                     let msgFolder = folder.QueryInterface(Ci.nsIMsgFolder);
33                     autocommands.trigger("FolderLoaded", { url: msgFolder });
34
35                     // Jump to a message when requested
36                     let indices = [];
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]]);
40
41                         indices.sort();
42                         let index = mail._selectMessageCount - 1;
43                         if (mail._selectMessageReverse)
44                             index = mail._selectMessageKeys.length - 1 - index;
45
46                         gDBView.selectMsgByKey(indices[index][1]);
47                         mail._selectMessageKeys = [];
48                     }
49                 }
50             }
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") {}*/
58         }
59     },
60
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]));
64     },
65
66     _getRSSUrl: function () {
67         return gDBView.hdrForFirstSelectedMessage.messageId.replace(/(#.*)?@.*$/, "");
68     },
69
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));
76
77         let count = gDBView.selection.count;
78         if (!count)
79             return void dactyl.beep();
80
81         (copy ? MsgCopyMessage : MsgMoveMessage)(folders[0]);
82         util.timeout(function () {
83             dactyl.echomsg(count + " message(s) " + (copy ? "copied" : "moved") + " to " + folders[0].prettyName, 1);
84         }, 100);
85     },
86
87     _parentIndex: function (index) {
88         let parent = index;
89         let tree = GetThreadTree();
90
91         while (true) {
92             let tmp = tree.view.getParentIndex(parent);
93             if (tmp >= 0)
94                 parent = tmp;
95             else
96                 break;
97         }
98         return parent;
99     },
100
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();
106         let i = direction;
107         let folder;
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)) {
111                 count -= 1;
112                 folder = i;
113             }
114             i += direction;
115         }
116         if (!folder || count > 0)
117             dactyl.beep();
118         else
119             gFolderTreeView.selection.timedSelect(c + folder, 500);
120     },
121
122     _escapeRecipient: function (recipient) {
123         // strip all ":
124         recipient = recipient.replace(/"/g, "");
125         return "\"" + recipient + "\"";
126     },
127
128     get currentAccount() this.currentFolder.rootFolder,
129
130     get currentFolder() gFolderTreeView.getSelectedFolders()[0],
131
132     /** @property {[nsISmtpServer]} The list of configured SMTP servers. */
133     get smtpServers() {
134         let servers = services.smtp.smtpServers;
135         let res = [];
136
137         while (servers.hasMoreElements()) {
138             let server = servers.getNext();
139             if (server instanceof Ci.nsISmtpServer)
140                 res.push(server);
141         }
142
143         return res;
144     },
145
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);
149
150         if (args) {
151             if (args.originalMsg)
152                 params.originalMsgURI = args.originalMsg;
153             if (args.to)
154                 params.composeFields.to = args.to;
155             if (args.cc)
156                 params.composeFields.cc = args.cc;
157             if (args.bcc)
158                 params.composeFields.bcc = args.bcc;
159             if (args.newsgroups)
160                 params.composeFields.newsgroups = args.newsgroups;
161             if (args.subject)
162                 params.composeFields.subject = args.subject;
163             if (args.body)
164                 params.composeFields.body = args.body;
165
166             if (args.attachments) {
167                 while (args.attachments.length > 0) {
168                     let url = args.attachments.pop();
169                     let file = io.getFile(url);
170                     if (!file.exists())
171                         return void dactyl.echoerr(_("mail.cantAttachFile", url), commandline.FORCE_SINGLELINE);
172
173                     attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment);
174                     attachment.url = "file://" + file.path;
175                     params.composeFields.addAttachment(attachment);
176                 }
177             }
178         }
179
180         params.type = Ci.nsIMsgCompType.New;
181
182         services.compose.OpenComposeWindowWithParams(null, params);
183     },
184
185     // returns an array of nsIMsgFolder objects
186     getFolders: function (filter, includeServers, includeMsgFolders) {
187         let folders = [];
188         if (!filter)
189             filter = "";
190         else
191             filter = filter.toLowerCase();
192
193         if (includeServers === undefined)
194             includeServers = false;
195         if (includeMsgFolders === undefined)
196             includeMsgFolders = true;
197
198         for (let i = 0; i < gFolderTreeView.rowCount; i++) {
199             let resource = gFolderTreeView._rowMap[i]._folder;
200             if ((resource.isServer && !includeServers) || (!resource.isServer && !includeMsgFolders))
201                 continue;
202
203             let folderString = resource.server.prettyName + ": " + resource.name;
204
205             if (resource.prettiestName.toLowerCase().indexOf(filter) >= 0)
206                 folders.push(resource);
207             else if (folderString.toLowerCase().indexOf(filter) >= 0)
208                 folders.push(resource);
209         }
210         return folders;
211     },
212
213     getNewMessages: function (currentAccountOnly) {
214         if (currentAccountOnly)
215             MsgGetMessagesForAccount();
216         else
217             GetMessagesForAllAuthenticatedAccounts();
218     },
219
220     getStatistics: function (currentAccountOnly) {
221         let accounts = currentAccountOnly ? [this.currentAccount]
222                                           : this.getFolders("", true, false);
223
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);
230         }
231
232         return { numUnread: unreadCount, numTotal: totalCount, numNew: newCount };
233     },
234
235     collapseThread: function () {
236         let tree = GetThreadTree();
237         if (tree) {
238             let parent = this._parentIndex(tree.currentIndex);
239             if (tree.changeOpenState(parent, false)) {
240                 tree.view.selection.select(parent);
241                 tree.treeBoxObject.ensureRowIsVisible(parent);
242                 return true;
243             }
244         }
245         return false;
246     },
247
248     expandThread: function () {
249         let tree = GetThreadTree();
250         if (tree) {
251             let row = tree.currentIndex;
252             if (row >= 0 && tree.changeOpenState(row, true))
253                return true;
254         }
255         return false;
256     },
257
258     /**
259      * General-purpose method to find messages.
260      *
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.
266      */
267     selectMessage: function (validatorFunc, canWrap, openThreads, reverse, count) {
268         function currentIndex() {
269             let index = gDBView.selection.currentIndex;
270             if (index < 0)
271                 index = 0;
272             return index;
273         }
274
275         function closedThread(index) {
276             if (!(gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay))
277                 return false;
278
279             index = (typeof index == "number") ? index : currentIndex();
280             return !gDBView.isContainerOpen(index) && !gDBView.isContainerEmpty(index);
281         }
282
283         if (typeof validatorFunc != "function")
284             return;
285
286         if (typeof count != "number" || count < 1)
287             count = 1;
288
289         // first try to find in current folder
290         if (gDBView) {
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);
296
297                 // a closed thread
298                 if (openThreads && closedThread(i)) {
299                     let thread = gDBView.db.GetThreadContainingMsgHdr(msg);
300                     let originalCount = count;
301
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);
311                             if (j > 0) {
312                                 GetThreadTree().changeOpenState(i, true);
313                                 this.selectMessage(validatorFunc, false, false, false, originalCount);
314                             }
315                             return;
316                         }
317                     }
318                 }
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);
323                         return;
324                     }
325                 }
326             }
327         }
328
329         // then in other folders
330         if (canWrap) {
331             this._selectMessageReverse = reverse;
332
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;
337                 if (reverse)
338                     index = folders.length - 1 - index;
339
340                 let folder = folders[index];
341                 if (folder.isServer)
342                     continue;
343
344                 this._selectMessageCount = count;
345                 this._selectMessageKeys = [];
346
347                 // sometimes folder.getMessages can fail with an exception
348                 // TODO: find out why, and solve the problem
349                 try {
350                     var msgs = folder.messages;
351                 }
352                 catch (e) {
353                     msgs = folder.getMessages(msgWindow); // for older thunderbirds
354                     dactyl.dump("WARNING: " + folder.prettyName + " failed to getMessages, trying old API");
355                     //continue;
356                 }
357
358                 while (msgs.hasMoreElements()) {
359                     let msg = msgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
360                     if (validatorFunc(msg)) {
361                         count--;
362                         this._selectMessageKeys.push(msg.messageKey);
363                     }
364                 }
365
366                 if (count <= 0) {
367                     // SelectFolder is asynchronous, message is selected in this._folderListener
368                     SelectFolder(folder.URI);
369                     return;
370                 }
371             }
372         }
373
374         // TODO: finally for the "rest" of the current folder
375
376         dactyl.beep();
377     },
378
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
383
384         if (typeof value != "number" || value < 0 || value > 2)
385             value = 1;
386
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]);
390         ReloadMessage();
391     }
392 }, {
393 }, {
394     commands: function initCommands(dactyl, modules, window) {
395         commands.add(["go[to]"],
396             "Select a folder",
397             function (args) {
398                 let count = Math.max(0, args.count - 1);
399                 let arg = args.literalArg || "Inbox";
400
401                 let folder = mail.getFolders(arg, true, true)[count];
402                 if (!folder)
403                     dactyl.echoerr(_("command.goto.folderNotExist", arg));
404                 else if (dactyl.forceNewTab)
405                     MsgOpenNewTabForFolder(folder.URI);
406                 else
407                     SelectFolder(folder.URI);
408             },
409             {
410                 argCount: "?",
411                 completer: function (context) completion.mailFolder(context),
412                 count: true,
413                 literal: 0
414             });
415
416         commands.add(["m[ail]"],
417             "Write a new message",
418             function (args) {
419                 let mailargs = {};
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"] || [];
426
427                 let addresses = args;
428                 if (mailargs.bcc)
429                     addresses = addresses.concat(mailargs.bcc);
430                 if (mailargs.cc)
431                     addresses = addresses.concat(mailargs.cc);
432
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"));
436
437                 mail.composeNewMail(mailargs);
438             },
439             {
440                 // TODO: completers, validators - whole shebang. Do people actually use this? --djk
441                 options: [
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"}
447                 ]
448             });
449
450         commands.add(["copy[to]"],
451             "Copy selected messages",
452             function (args) { mail._moveOrCopy(true, args.literalArg); },
453             {
454                 argCount: "1",
455                 completer: function (context) completion.mailFolder(context),
456                 literal: 0
457             });
458
459         commands.add(["move[to]"],
460             "Move selected messages",
461             function (args) { mail._moveOrCopy(false, args.literalArg); },
462             {
463                 argCount: "1",
464                 completer: function (context) completion.mailFolder(context),
465                 literal: 0
466             });
467
468         commands.add(["empty[trash]"],
469             "Empty trash of the current account",
470             function () { window.goDoCommand("cmd_emptyTrash"); },
471             { argCount: "0" });
472
473         commands.add(["get[messages]"],
474             "Check for new messages",
475             function (args) { mail.getNewMessages(!args.bang); },
476             {
477                 argCount: "0",
478                 bang: true,
479             });
480     },
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)]);
489         };
490     },
491     mappings: function initMappings(dactyl, modules, window) {
492         var myModes = config.mailModes;
493
494         mappings.add(myModes, ["<Return>", "i"],
495             "Inspect (focus) message",
496             function () { content.focus(); });
497
498         mappings.add(myModes, ["I"],
499             "Open the message in new tab",
500             function () {
501                 if (gDBView && gDBView.selection.count < 1)
502                     return void dactyl.beep();
503
504                 MsgOpenNewTabForMessage();
505             });
506
507         mappings.add(myModes, ["<Space>"],
508             "Scroll message or select next unread one",
509             () => Events.PASS);
510
511         mappings.add(myModes, ["t"],
512             "Select thread",
513             function () { gDBView.ExpandAndSelectThreadByIndex(GetThreadTree().currentIndex, false); });
514
515         mappings.add(myModes, ["d", "<Del>"],
516             "Move mail to Trash folder",
517             function () { window.goDoCommand("cmd_delete"); });
518
519         mappings.add(myModes, ["j", "<Right>"],
520             "Select next message",
521             function ({ count }) { mail.selectMessage(msg => true, false, false, false, count); },
522             { count: true });
523
524         mappings.add(myModes, ["gj"],
525             "Select next message, including closed threads",
526             function ({ count }) { mail.selectMessage(msg => true, false, true, false, count); },
527             { count: true });
528
529         mappings.add(myModes, ["J", "<Tab>"],
530             "Select next unread message",
531             function ({ count }) { mail.selectMessage(msg => !msg.isRead, true, true, false, count); },
532             { count: true });
533
534         mappings.add(myModes, ["k", "<Left>"],
535             "Select previous message",
536             function ({ count }) { mail.selectMessage(msg => true, false, false, true, count); },
537             { count: true });
538
539         mappings.add(myModes, ["gk"],
540             "Select previous message",
541             function ({ count }) { mail.selectMessage(msg => true, false, true, true, count); },
542             { count: true });
543
544         mappings.add(myModes, ["K"],
545             "Select previous unread message",
546             function ({ count }) { mail.selectMessage(msg => !msg.isRead, true, true, true, count); },
547             { count: true });
548
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);
554             },
555             { count: true });
556
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);
562             },
563             { count: true });
564
565         // SENDING MESSAGES
566         mappings.add(myModes, ["m"],
567             "Compose a new message",
568             function () { CommandExMode().open("mail -subject="); });
569
570         mappings.add(myModes, ["M"],
571             "Compose a new message to the sender of selected mail",
572             function () {
573                 let to = mail._escapeRecipient(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor);
574                 CommandExMode().open("mail " + to + " -subject=");
575             });
576
577         mappings.add(myModes, ["r"],
578             "Reply to sender",
579             function () { window.goDoCommand("cmd_reply"); });
580
581         mappings.add(myModes, ["R"],
582             "Reply to all",
583             function () { window.goDoCommand("cmd_replyall"); });
584
585         mappings.add(myModes, ["f"],
586             "Forward message",
587             function () { window.goDoCommand("cmd_forward"); });
588
589         mappings.add(myModes, ["F"],
590             "Forward message inline",
591             function () { window.goDoCommand("cmd_forwardInline"); });
592
593         // SCROLLING
594         mappings.add(myModes, ["<Down>"],
595             "Scroll message down",
596             function ({ count }) { buffer.scrollLines(Math.max(count, 1)); },
597             { count: true });
598
599         mappings.add(myModes, ["<Up>"],
600             "Scroll message up",
601             function ({ count }) { buffer.scrollLines(-Math.max(count, 1)); },
602             { count: true });
603
604         mappings.add([modes.MESSAGE], ["<Left>"],
605             "Select previous message",
606             function ({ count }) { mail.selectMessage(msg => true, false, false, true, count); },
607             { count: true });
608
609         mappings.add([modes.MESSAGE], ["<Right>"],
610             "Select next message",
611             function ({ count }) { mail.selectMessage(msg => true, false, false, false, count); },
612             { count: true });
613
614         // UNDO/REDO
615         mappings.add(myModes, ["u"],
616             "Undo",
617             function () {
618                 if (messenger.canUndo())
619                     messenger.undo(msgWindow);
620                 else
621                     dactyl.beep();
622             });
623         mappings.add(myModes, ["<C-r>"],
624             "Redo",
625             function () {
626                 if (messenger.canRedo())
627                     messenger.redo(msgWindow);
628                 else
629                     dactyl.beep();
630             });
631
632         // GETTING MAIL
633         mappings.add(myModes, ["gm"],
634             "Get new messages",
635             function () { mail.getNewMessages(); });
636
637         mappings.add(myModes, ["gM"],
638             "Get new messages for current account only",
639             function () { mail.getNewMessages(true); });
640
641         // MOVING MAIL
642         mappings.add(myModes, ["c"],
643             "Change folders",
644             function () { CommandExMode().open("goto "); });
645
646         mappings.add(myModes, ["s"],
647             "Move selected messages",
648             function () { CommandExMode().open("moveto "); });
649
650         mappings.add(myModes, ["S"],
651             "Copy selected messages",
652             function () { CommandExMode().open("copyto "); });
653
654         mappings.add(myModes, ["<C-s>"],
655             "Archive message",
656             function () { mail._moveOrCopy(false, options["archivefolder"]); });
657
658         mappings.add(myModes, ["]s"],
659             "Select next starred message",
660             function ({ count }) { mail.selectMessage(msg => msg.isFlagged, true, true, false, count); },
661             { count: true });
662
663         mappings.add(myModes, ["[s"],
664             "Select previous starred message",
665             function ({ count }) { mail.selectMessage(msg => msg.isFlagged, true, true, true, count); },
666             { count: true });
667
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); },
671             { count: true });
672
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); },
676             { count: true });
677
678         // FOLDER SWITCHING
679         mappings.add(myModes, ["gi"],
680             "Go to inbox",
681             function ({ count }) {
682                 let folder = mail.getFolders("Inbox", false, true)[(count > 0) ? (count - 1) : 0];
683                 if (folder)
684                     SelectFolder(folder.URI);
685                 else
686                     dactyl.beep();
687             },
688             { count: true });
689
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);
697                 }
698                 gFolderTreeView.selection.timedSelect(newPos, 500);
699             },
700             { count: true });
701
702         mappings.add(myModes, ["<C-N>"],
703             "Go to next mailbox with unread messages",
704             function ({ count }) {
705                 mail._selectUnreadFolder(false, count);
706             },
707             { count: true });
708
709         mappings.add(myModes, ["<C-p>"],
710             "Select previous folder",
711             function ({ count }) {
712                 let newPos = mail._getCurrentFolderIndex() - Math.max(1, count);
713                 if (newPos < 0) {
714                     newPos = (newPos % gFolderTreeView.rowCount) + gFolderTreeView.rowCount;
715                     commandline.echo(_("finder.atTop"), commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
716                 }
717                 gFolderTreeView.selection.timedSelect(newPos, 500);
718             },
719             { count: true });
720
721         mappings.add(myModes, ["<C-P>"],
722             "Go to previous mailbox with unread messages",
723             function ({ count }) {
724                 mail._selectUnreadFolder(true, count);
725             },
726             { count: true });
727
728         // THREADING
729         mappings.add(myModes, ["za"],
730             "Toggle thread collapsed/expanded",
731             function () { if (!mail.expandThread()) mail.collapseThread(); });
732
733         mappings.add(myModes, ["zc"],
734             "Collapse thread",
735             function () { mail.collapseThread(); });
736
737         mappings.add(myModes, ["zo"],
738             "Open thread",
739             function () { mail.expandThread(); });
740
741         mappings.add(myModes, ["zr", "zR"],
742             "Expand all threads",
743             function () { window.goDoCommand("cmd_expandAllThreads"); });
744
745         mappings.add(myModes, ["zm", "zM"],
746             "Collapse all threads",
747             function () { window.goDoCommand("cmd_collapseAllThreads"); });
748
749         mappings.add(myModes, ["<C-i>"],
750             "Go forward",
751             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.forward, true); },
752             { count: true });
753
754         mappings.add(myModes, ["<C-o>"],
755             "Go back",
756             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.back, true); },
757             { count: true });
758
759         mappings.add(myModes, ["gg"],
760             "Select first message",
761             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.firstMessage, true); },
762             { count: true });
763
764         mappings.add(myModes, ["G"],
765             "Select last message",
766             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.lastMessage, false); },
767             { count: true });
768
769         // tagging messages
770         mappings.add(myModes, ["l"],
771             "Label message",
772             function ({ arg }) {
773                 if (!GetSelectedMessages())
774                     return void dactyl.beep();
775
776                 switch (arg) {
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();
785                 }
786             },
787             {
788                 arg: true
789             });
790
791         // TODO: change binding?
792         mappings.add(myModes, ["T"],
793             "Mark current folder as read",
794             function () {
795                 if (mail.currentFolder.isServer)
796                     return dactyl.beep();
797
798                 mail.currentFolder.markAllMessagesRead(msgWindow);
799             });
800
801         mappings.add(myModes, ["<C-t>"],
802             "Mark all messages as read",
803             function () {
804                 mail.getFolders("", false).forEach(function (folder) { folder.markAllMessagesRead(msgWindow); });
805             });
806
807         // DISPLAY OPTIONS
808         mappings.add(myModes, ["h"],
809             "Toggle displayed headers",
810             function () {
811                 let value = gPrefBranch.getIntPref("mail.show_headers", 2);
812                 gPrefBranch.setIntPref("mail.show_headers", value == 2 ? 1 : 2);
813                 ReloadMessage();
814             });
815
816         mappings.add(myModes, ["x"],
817             "Toggle HTML message display",
818             function () {
819                 let wantHtml = (gPrefBranch.getIntPref("mailnews.display.html_as", 1) == 1);
820                 mail.setHTML(wantHtml ? 1 : 0);
821             });
822
823         // YANKING TEXT
824         mappings.add(myModes, ["Y"],
825             "Yank subject",
826             function () {
827                 try {
828                     let subject = gDBView.hdrForFirstSelectedMessage.mime2DecodedSubject;
829                     dactyl.clipboardWrite(subject, true);
830                 }
831                 catch (e) { dactyl.beep(); }
832             });
833
834         mappings.add(myModes, ["y"],
835             "Yank sender or feed URL",
836             function () {
837                 try {
838                     if (mail.currentAccount.server.type == "rss")
839                         dactyl.clipboardWrite(mail._getRSSUrl(), true);
840                     else
841                         dactyl.clipboardWrite(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor, true);
842                 }
843                 catch (e) { dactyl.beep(); }
844             });
845
846         // RSS specific mappings
847         mappings.add(myModes, ["p"],
848             "Open RSS message in browser",
849             function () {
850                 try {
851                     if (mail.currentAccount.server.type == "rss")
852                         messenger.launchExternalURL(mail._getRSSUrl());
853                     // TODO: what to do for non-rss message?
854                 }
855                 catch (e) {
856                     dactyl.beep();
857                 }
858             });
859     },
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");
863     },
864     modes: function initModes(dactyl, modules, window) {
865         modes.addMode("MESSAGE", {
866             char: "m",
867             description: "Active the message is focused",
868             bases: [modes.COMMAND]
869         });
870     },
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",
876             "string", "Archive",
877             {
878                 completer: function (context) completion.mailFolder(context)
879             });
880
881         // TODO: generate the possible values dynamically from the menu
882         options.add(["layout"],
883             "Set the layout of the mail window",
884             "string", "inherit",
885             {
886                 setter: function (value) {
887                     switch (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
892                     }
893
894                     return value;
895                 },
896                 completer: function (context) [
897                     ["inherit",  "Default View"], // FIXME: correct description?
898                     ["classic",  "Classic View"],
899                     ["wide",     "Wide View"],
900                     ["vertical", "Vertical View"]
901                 ]
902             });
903
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?
907             {
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;
912                     return value;
913                 },
914                 completer: function (context) [[s.key, s.serverURI] for ([, s] in Iterator(mail.smtpServers))]
915             });
916
917         /*options.add(["threads"],
918             "Use threading to group messages",
919             "boolean", true,
920             {
921                 setter: function (value) {
922                     if (value)
923                         MsgSortThreaded();
924                     else
925                         MsgSortUnthreaded();
926
927                     return value;
928                 }
929             });*/
930     }
931 });
932
933 // vim: set fdm=marker sw=4 sts=4 ts=8 et: