]> git.donarmstrong.com Git - dactyl.git/blob - teledactyl/content/mail.js
ff01820c6eb57555861fae186f3cf972644c8612
[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 const 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("Exxx: No matching folder for " + destinationFolder);
74         else if (folders.length > 1)
75             return dactyl.echoerr("Exxx: More than one match for " + 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("Exxx: Could not attach file `" + 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         const msgComposeService = Cc["@mozilla.org/messengercompose;1"].getService();
183         msgComposeService = msgComposeService.QueryInterface(Ci.nsIMsgComposeService);
184         msgComposeService.OpenComposeWindowWithParams(null, params);
185     },
186
187     // returns an array of nsIMsgFolder objects
188     getFolders: function (filter, includeServers, includeMsgFolders) {
189         let folders = [];
190         if (!filter)
191             filter = "";
192         else
193             filter = filter.toLowerCase();
194
195         if (includeServers === undefined)
196             includeServers = false;
197         if (includeMsgFolders === undefined)
198             includeMsgFolders = true;
199
200         for (let i = 0; i < gFolderTreeView.rowCount; i++) {
201             let resource = gFolderTreeView._rowMap[i]._folder;
202             if ((resource.isServer && !includeServers) || (!resource.isServer && !includeMsgFolders))
203                 continue;
204
205             let folderString = resource.server.prettyName + ": " + resource.name;
206
207             if (resource.prettiestName.toLowerCase().indexOf(filter) >= 0)
208                 folders.push(resource);
209             else if (folderString.toLowerCase().indexOf(filter) >= 0)
210                 folders.push(resource);
211         }
212         return folders;
213     },
214
215     getNewMessages: function (currentAccountOnly) {
216         if (currentAccountOnly)
217             MsgGetMessagesForAccount();
218         else
219             GetMessagesForAllAuthenticatedAccounts();
220     },
221
222     getStatistics: function (currentAccountOnly) {
223         let accounts = currentAccountOnly ? [this.currentAccount]
224                                           : this.getFolders("", true, false);
225
226         let unreadCount = 0, totalCount = 0, newCount = 0;
227         for (let i = 0; i < accounts.length; i++) {
228             let account = accounts[i];
229             unreadCount += account.getNumUnread(true); // true == deep (includes subfolders)
230             totalCount  += account.getTotalMessages(true);
231             newCount    += account.getNumUnread(true);
232         }
233
234         return { numUnread: unreadCount, numTotal: totalCount, numNew: newCount };
235     },
236
237     collapseThread: function () {
238         let tree = GetThreadTree();
239         if (tree) {
240             let parent = this._parentIndex(tree.currentIndex);
241             if (tree.changeOpenState(parent, false)) {
242                 tree.view.selection.select(parent);
243                 tree.treeBoxObject.ensureRowIsVisible(parent);
244                 return true;
245             }
246         }
247         return false;
248     },
249
250     expandThread: function () {
251         let tree = GetThreadTree();
252         if (tree) {
253             let row = tree.currentIndex;
254             if (row >= 0 && tree.changeOpenState(row, true))
255                return true;
256         }
257         return false;
258     },
259
260     /**
261      * General-purpose method to find messages.
262      *
263      * @param {function(nsIMsgDBHdr):boolean} validatorFunc Return
264      *     true/false whether msg should be selected or not.
265      * @param {boolean} canWrap When true, wraps around folders.
266      * @param {boolean} openThreads Should we open closed threads?
267      * @param {boolean} reverse Change direction of searching.
268      */
269     selectMessage: function (validatorFunc, canWrap, openThreads, reverse, count) {
270         function currentIndex() {
271             let index = gDBView.selection.currentIndex;
272             if (index < 0)
273                 index = 0;
274             return index;
275         }
276
277         function closedThread(index) {
278             if (!(gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay))
279                 return false;
280
281             index = (typeof index == "number") ? index : currentIndex();
282             return !gDBView.isContainerOpen(index) && !gDBView.isContainerEmpty(index);
283         }
284
285         if (typeof validatorFunc != "function")
286             return;
287
288         if (typeof count != "number" || count < 1)
289             count = 1;
290
291         // first try to find in current folder
292         if (gDBView) {
293             for (let i = currentIndex() + (reverse ? -1 : (openThreads && closedThread() ? 0 : 1));
294                     reverse ? (i >= 0) : (i < gDBView.rowCount);
295                     reverse ? i-- : i++) {
296                 let key = gDBView.getKeyAt(i);
297                 let msg = gDBView.db.GetMsgHdrForKey(key);
298
299                 // a closed thread
300                 if (openThreads && closedThread(i)) {
301                     let thread = gDBView.db.GetThreadContainingMsgHdr(msg);
302                     let originalCount = count;
303
304                     for (let j = (i == currentIndex() && !reverse) ? 1 : (reverse ? thread.numChildren - 1 : 0);
305                              reverse ? (j >= 0) : (j < thread.numChildren);
306                              reverse ? j-- : j++) {
307                         msg = thread.getChildAt(j);
308                         if (validatorFunc(msg) && --count == 0) {
309                             // this hack is needed to get the correct message, because getChildAt() does not
310                             // necessarily return the messages in the order they are displayed
311                             gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500);
312                             GetThreadTree().treeBoxObject.ensureRowIsVisible(i);
313                             if (j > 0) {
314                                 GetThreadTree().changeOpenState(i, true);
315                                 this.selectMessage(validatorFunc, false, false, false, originalCount);
316                             }
317                             return;
318                         }
319                     }
320                 }
321                 else { // simple non-threaded message
322                     if (validatorFunc(msg) && --count == 0) {
323                         gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500);
324                         GetThreadTree().treeBoxObject.ensureRowIsVisible(i);
325                         return;
326                     }
327                 }
328             }
329         }
330
331         // then in other folders
332         if (canWrap) {
333             this._selectMessageReverse = reverse;
334
335             let folders = this.getFolders("", true, true);
336             let ci = this._getCurrentFolderIndex();
337             for (let i = 1; i < folders.length; i++) {
338                 let index = (i + ci) % folders.length;
339                 if (reverse)
340                     index = folders.length - 1 - index;
341
342                 let folder = folders[index];
343                 if (folder.isServer)
344                     continue;
345
346                 this._selectMessageCount = count;
347                 this._selectMessageKeys = [];
348
349                 // sometimes folder.getMessages can fail with an exception
350                 // TODO: find out why, and solve the problem
351                 try {
352                     var msgs = folder.messages;
353                 }
354                 catch (e) {
355                     msgs = folder.getMessages(msgWindow); // for older thunderbirds
356                     dactyl.dump("WARNING: " + folder.prettyName + " failed to getMessages, trying old API");
357                     //continue;
358                 }
359
360                 while (msgs.hasMoreElements()) {
361                     let msg = msgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
362                     if (validatorFunc(msg)) {
363                         count--;
364                         this._selectMessageKeys.push(msg.messageKey);
365                     }
366                 }
367
368                 if (count <= 0) {
369                     // SelectFolder is asynchronous, message is selected in this._folderListener
370                     SelectFolder(folder.URI);
371                     return;
372                 }
373             }
374         }
375
376         // TODO: finally for the "rest" of the current folder
377
378         dactyl.beep();
379     },
380
381     setHTML: function (value) {
382         let values = [[true,  1, gDisallow_classes_no_html],  // plaintext
383                       [false, 0, 0],                          // HTML
384                       [false, 3, gDisallow_classes_no_html]]; // sanitized/simple HTML
385
386         if (typeof value != "number" || value < 0 || value > 2)
387             value = 1;
388
389         gPrefBranch.setBoolPref("mailnews.display.prefer_plaintext", values[value][0]);
390         gPrefBranch.setIntPref("mailnews.display.html_as", values[value][1]);
391         gPrefBranch.setIntPref("mailnews.display.disallow_mime_handlers", values[value][2]);
392         ReloadMessage();
393     }
394 }, {
395 }, {
396     commands: function initCommands(dactyl, modules, window) {
397         commands.add(["go[to]"],
398             "Select a folder",
399             function (args) {
400                 let count = Math.max(0, args.count - 1);
401                 let arg = args.literalArg || "Inbox";
402
403                 let folder = mail.getFolders(arg, true, true)[count];
404                 if (!folder)
405                     dactyl.echoerr("Exxx: Folder \"" + arg + "\" does not exist");
406                 else if (dactyl.forceNewTab)
407                     MsgOpenNewTabForFolder(folder.URI);
408                 else
409                     SelectFolder(folder.URI);
410             },
411             {
412                 argCount: "?",
413                 completer: function (context) completion.mailFolder(context),
414                 count: true,
415                 literal: 0
416             });
417
418         commands.add(["m[ail]"],
419             "Write a new message",
420             function (args) {
421                 let mailargs = {};
422                 mailargs.to =          args.join(", ");
423                 mailargs.subject =     args["-subject"];
424                 mailargs.bcc =         args["-bcc"] || [];
425                 mailargs.cc =          args["-cc"] || [];
426                 mailargs.body =        args["-text"];
427                 mailargs.attachments = args["-attachment"] || [];
428
429                 let addresses = args;
430                 if (mailargs.bcc)
431                     addresses = addresses.concat(mailargs.bcc);
432                 if (mailargs.cc)
433                     addresses = addresses.concat(mailargs.cc);
434
435                 // TODO: is there a better way to check for validity?
436                 if (addresses.some(function (recipient) !(/\S@\S+\.\S/.test(recipient))))
437                     return void dactyl.echoerr("Exxx: Invalid e-mail address");
438
439                 mail.composeNewMail(mailargs);
440             },
441             {
442                 // TODO: completers, validators - whole shebang. Do people actually use this? --djk
443                 options: [
444                     { names: ["-subject", "-s"],     type: CommandOption.STRING, description: "Subject line"},
445                     { names: ["-attachment", "-a"],  type: CommandOption.LIST,   description: "List of attachments"},
446                     { names: ["-bcc", "-b"],         type: CommandOption.LIST,   description: "Blind Carbon Copy addresses"},
447                     { names: ["-cc", "-c"],          type: CommandOption.LIST,   description: "Carbon Copy addresses"},
448                     { names: ["-text", "-t"],        type: CommandOption.STRING, description: "Message body"}
449                 ]
450             });
451
452         commands.add(["copy[to]"],
453             "Copy selected messages",
454             function (args) { mail._moveOrCopy(true, args.literalArg); },
455             {
456                 argCount: 1,
457                 completer: function (context) completion.mailFolder(context),
458                 literal: 0
459             });
460
461         commands.add(["move[to]"],
462             "Move selected messages",
463             function (args) { mail._moveOrCopy(false, args.literalArg); },
464             {
465                 argCount: 1,
466                 completer: function (context) completion.mailFolder(context),
467                 literal: 0
468             });
469
470         commands.add(["empty[trash]"],
471             "Empty trash of the current account",
472             function () { window.goDoCommand("cmd_emptyTrash"); },
473             { argCount: "0" });
474
475         commands.add(["get[messages]"],
476             "Check for new messages",
477             function (args) mail.getNewMessages(!args.bang),
478             {
479                 argCount: "0",
480                 bang: true,
481             });
482     },
483     completion: function initCompletion(dactyl, modules, window) {
484         completion.mailFolder = function mailFolder(context) {
485             let folders = mail.getFolders(context.filter);
486             context.anchored = false;
487             context.quote = false;
488             context.completions = folders.map(function (folder)
489                     [folder.server.prettyName + ": " + folder.name,
490                      "Unread: " + folder.getNumUnread(false)]);
491         };
492     },
493     mappings: function initMappings(dactyl, modules, window) {
494         var myModes = config.mailModes;
495
496         mappings.add(myModes, ["<Return>", "i"],
497             "Inspect (focus) message",
498             function () { content.focus(); });
499
500         mappings.add(myModes, ["I"],
501             "Open the message in new tab",
502             function () {
503                 if (gDBView && gDBView.selection.count < 1)
504                     return void dactyl.beep();
505
506                 MsgOpenNewTabForMessage();
507             });
508
509         mappings.add(myModes, ["<Space>"],
510             "Scroll message or select next unread one",
511             function () Events.PASS);
512
513         mappings.add(myModes, ["t"],
514             "Select thread",
515             function () { gDBView.ExpandAndSelectThreadByIndex(GetThreadTree().currentIndex, false); });
516
517         mappings.add(myModes, ["d", "<Del>"],
518             "Move mail to Trash folder",
519             function () { window.goDoCommand("cmd_delete"); });
520
521         mappings.add(myModes, ["j", "<Right>"],
522             "Select next message",
523             function (args) { mail.selectMessage(function (msg) true, false, false, false, args.count); },
524             { count: true });
525
526         mappings.add(myModes, ["gj"],
527             "Select next message, including closed threads",
528             function (args) { mail.selectMessage(function (msg) true, false, true, false, args.count); },
529             { count: true });
530
531         mappings.add(myModes, ["J", "<Tab>"],
532             "Select next unread message",
533             function (args) { mail.selectMessage(function (msg) !msg.isRead, true, true, false, args.count); },
534             { count: true });
535
536         mappings.add(myModes, ["k", "<Left>"],
537             "Select previous message",
538             function (args) { mail.selectMessage(function (msg) true, false, false, true, args.count); },
539             { count: true });
540
541         mappings.add(myModes, ["gk"],
542             "Select previous message",
543             function (args) { mail.selectMessage(function (msg) true, false, true, true, args.count); },
544             { count: true });
545
546         mappings.add(myModes, ["K"],
547             "Select previous unread message",
548             function (args) { mail.selectMessage(function (msg) !msg.isRead, true, true, true, args.count); },
549             { count: true });
550
551         mappings.add(myModes, ["*"],
552             "Select next message from the same sender",
553             function (args) {
554                 let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase();
555                 mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, false, args.count);
556             },
557             { count: true });
558
559         mappings.add(myModes, ["#"],
560             "Select previous message from the same sender",
561             function (args) {
562                 let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase();
563                 mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, true, args.count);
564             },
565             { count: true });
566
567         // SENDING MESSAGES
568         mappings.add(myModes, ["m"],
569             "Compose a new message",
570             function () { CommandExMode().open("mail -subject="); });
571
572         mappings.add(myModes, ["M"],
573             "Compose a new message to the sender of selected mail",
574             function () {
575                 let to = mail._escapeRecipient(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor);
576                 CommandExMode().open("mail " + to + " -subject=");
577             });
578
579         mappings.add(myModes, ["r"],
580             "Reply to sender",
581             function () { window.goDoCommand("cmd_reply"); });
582
583         mappings.add(myModes, ["R"],
584             "Reply to all",
585             function () { window.goDoCommand("cmd_replyall"); });
586
587         mappings.add(myModes, ["f"],
588             "Forward message",
589             function () { window.goDoCommand("cmd_forward"); });
590
591         mappings.add(myModes, ["F"],
592             "Forward message inline",
593             function () { window.goDoCommand("cmd_forwardInline"); });
594
595         // SCROLLING
596         mappings.add(myModes, ["<Down>"],
597             "Scroll message down",
598             function (args) { buffer.scrollLines(Math.max(args.count, 1)); },
599             { count: true });
600
601         mappings.add(myModes, ["<Up>"],
602             "Scroll message up",
603             function (args) { buffer.scrollLines(-Math.max(args.count, 1)); },
604             { count: true });
605
606         mappings.add([modes.MESSAGE], ["<Left>"],
607             "Select previous message",
608             function (args) { mail.selectMessage(function (msg) true, false, false, true, args.count); },
609             { count: true });
610
611         mappings.add([modes.MESSAGE], ["<Right>"],
612             "Select next message",
613             function (args) { mail.selectMessage(function (msg) true, false, false, false, args.count); },
614             { count: true });
615
616         // UNDO/REDO
617         mappings.add(myModes, ["u"],
618             "Undo",
619             function () {
620                 if (messenger.canUndo())
621                     messenger.undo(msgWindow);
622                 else
623                     dactyl.beep();
624             });
625         mappings.add(myModes, ["<C-r>"],
626             "Redo",
627             function () {
628                 if (messenger.canRedo())
629                     messenger.redo(msgWindow);
630                 else
631                     dactyl.beep();
632             });
633
634         // GETTING MAIL
635         mappings.add(myModes, ["gm"],
636             "Get new messages",
637             function () { mail.getNewMessages(); });
638
639         mappings.add(myModes, ["gM"],
640             "Get new messages for current account only",
641             function () { mail.getNewMessages(true); });
642
643         // MOVING MAIL
644         mappings.add(myModes, ["c"],
645             "Change folders",
646             function () { CommandExMode().open("goto "); });
647
648         mappings.add(myModes, ["s"],
649             "Move selected messages",
650             function () { CommandExMode().open("moveto "); });
651
652         mappings.add(myModes, ["S"],
653             "Copy selected messages",
654             function () { CommandExMode().open("copyto "); });
655
656         mappings.add(myModes, ["<C-s>"],
657             "Archive message",
658             function () { mail._moveOrCopy(false, options["archivefolder"]); });
659
660         mappings.add(myModes, ["]s"],
661             "Select next starred message",
662             function (args) { mail.selectMessage(function (msg) msg.isFlagged, true, true, false, args.count); },
663             { count: true });
664
665         mappings.add(myModes, ["[s"],
666             "Select previous starred message",
667             function (args) { mail.selectMessage(function (msg) msg.isFlagged, true, true, true, args.count); },
668             { count: true });
669
670         mappings.add(myModes, ["]a"],
671             "Select next message with an attachment",
672             function (args) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, false, args.count); },
673             { count: true });
674
675         mappings.add(myModes, ["[a"],
676             "Select previous message with an attachment",
677             function (args) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, true, args.count); },
678             { count: true });
679
680         // FOLDER SWITCHING
681         mappings.add(myModes, ["gi"],
682             "Go to inbox",
683             function (args) {
684                 let folder = mail.getFolders("Inbox", false, true)[(args.count > 0) ? (args.count - 1) : 0];
685                 if (folder)
686                     SelectFolder(folder.URI);
687                 else
688                     dactyl.beep();
689             },
690             { count: true });
691
692         mappings.add(myModes, ["<C-n>"],
693             "Select next folder",
694             function (args) {
695                 let newPos = mail._getCurrentFolderIndex() + Math.max(1, args.count);
696                 if (newPos >= gFolderTreeView.rowCount) {
697                     newPos = newPos % gFolderTreeView.rowCount;
698                     commandline.echo(_("finder.atBottom"), commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
699                 }
700                 gFolderTreeView.selection.timedSelect(newPos, 500);
701             },
702             { count: true });
703
704         mappings.add(myModes, ["<C-N>"],
705             "Go to next mailbox with unread messages",
706             function (args) {
707                 mail._selectUnreadFolder(false, args.count);
708             },
709             { count: true });
710
711         mappings.add(myModes, ["<C-p>"],
712             "Select previous folder",
713             function (args) {
714                 let newPos = mail._getCurrentFolderIndex() - Math.max(1, args.count);
715                 if (newPos < 0) {
716                     newPos = (newPos % gFolderTreeView.rowCount) + gFolderTreeView.rowCount;
717                     commandline.echo(_("finder.atTop"), commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
718                 }
719                 gFolderTreeView.selection.timedSelect(newPos, 500);
720             },
721             { count: true });
722
723         mappings.add(myModes, ["<C-P>"],
724             "Go to previous mailbox with unread messages",
725             function (args) {
726                 mail._selectUnreadFolder(true, args.count);
727             },
728             { count: true });
729
730         // THREADING
731         mappings.add(myModes, ["za"],
732             "Toggle thread collapsed/expanded",
733             function () { if (!mail.expandThread()) mail.collapseThread(); });
734
735         mappings.add(myModes, ["zc"],
736             "Collapse thread",
737             function () { mail.collapseThread(); });
738
739         mappings.add(myModes, ["zo"],
740             "Open thread",
741             function () { mail.expandThread(); });
742
743         mappings.add(myModes, ["zr", "zR"],
744             "Expand all threads",
745             function () { window.goDoCommand("cmd_expandAllThreads"); });
746
747         mappings.add(myModes, ["zm", "zM"],
748             "Collapse all threads",
749             function () { window.goDoCommand("cmd_collapseAllThreads"); });
750
751         mappings.add(myModes, ["<C-i>"],
752             "Go forward",
753             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.forward, true); },
754             { count: true });
755
756         mappings.add(myModes, ["<C-o>"],
757             "Go back",
758             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.back, true); },
759             { count: true });
760
761         mappings.add(myModes, ["gg"],
762             "Select first message",
763             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.firstMessage, true); },
764             { count: true });
765
766         mappings.add(myModes, ["G"],
767             "Select last message",
768             function ({ count }) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.lastMessage, false); },
769             { count: true });
770
771         // tagging messages
772         mappings.add(myModes, ["l"],
773             "Label message",
774             function (arg) {
775                 if (!GetSelectedMessages())
776                     return void dactyl.beep();
777
778                 switch (arg) {
779                     case "r": MsgMarkMsgAsRead(); break;
780                     case "s": MsgMarkAsFlagged(); break;
781                     case "i": ToggleMessageTagKey(1); break; // Important
782                     case "w": ToggleMessageTagKey(2); break; // Work
783                     case "p": ToggleMessageTagKey(3); break; // Personal
784                     case "t": ToggleMessageTagKey(4); break; // TODO
785                     case "l": ToggleMessageTagKey(5); break; // Later
786                     default:  dactyl.beep();
787                 }
788             },
789             {
790                 arg: true
791             });
792
793         // TODO: change binding?
794         mappings.add(myModes, ["T"],
795             "Mark current folder as read",
796             function () {
797                 if (mail.currentFolder.isServer)
798                     return dactyl.beep();
799
800                 mail.currentFolder.markAllMessagesRead(msgWindow);
801             });
802
803         mappings.add(myModes, ["<C-t>"],
804             "Mark all messages as read",
805             function () {
806                 mail.getFolders("", false).forEach(function (folder) { folder.markAllMessagesRead(msgWindow); });
807             });
808
809         // DISPLAY OPTIONS
810         mappings.add(myModes, ["h"],
811             "Toggle displayed headers",
812             function () {
813                 let value = gPrefBranch.getIntPref("mail.show_headers", 2);
814                 gPrefBranch.setIntPref("mail.show_headers", value == 2 ? 1 : 2);
815                 ReloadMessage();
816             });
817
818         mappings.add(myModes, ["x"],
819             "Toggle HTML message display",
820             function () {
821                 let wantHtml = (gPrefBranch.getIntPref("mailnews.display.html_as", 1) == 1);
822                 mail.setHTML(wantHtml ? 1 : 0);
823             });
824
825         // YANKING TEXT
826         mappings.add(myModes, ["Y"],
827             "Yank subject",
828             function () {
829                 try {
830                     let subject = gDBView.hdrForFirstSelectedMessage.mime2DecodedSubject;
831                     dactyl.clipboardWrite(subject, true);
832                 }
833                 catch (e) { dactyl.beep(); }
834             });
835
836         mappings.add(myModes, ["y"],
837             "Yank sender or feed URL",
838             function () {
839                 try {
840                     if (mail.currentAccount.server.type == "rss")
841                         dactyl.clipboardWrite(mail._getRSSUrl(), true);
842                     else
843                         dactyl.clipboardWrite(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor, true);
844                 }
845                 catch (e) { dactyl.beep(); }
846             });
847
848         // RSS specific mappings
849         mappings.add(myModes, ["p"],
850             "Open RSS message in browser",
851             function () {
852                 try {
853                     if (mail.currentAccount.server.type == "rss")
854                         messenger.launchExternalURL(mail._getRSSUrl());
855                     // TODO: what to do for non-rss message?
856                 }
857                 catch (e) {
858                     dactyl.beep();
859                 }
860             });
861     },
862     services: function initServices(dactyl, modules, window) {
863         services.add("smtp", "@mozilla.org/messengercompose/smtp;1", Ci.nsISmtpService);
864     },
865
866     modes: function initModes(dactyl, modules, window) {
867         modes.addMode("MESSAGE", {
868             char: "m",
869             description: "Active the message is focused",
870             bases: [modes.COMMAND]
871         });
872     },
873     options: function initOptions(dactyl, modules, window) {
874         // FIXME: why does this default to "Archive", I don't have one? The default
875         // value won't validate now. mst please fix. --djk
876         options.add(["archivefolder"],
877             "Set the archive folder",
878             "string", "Archive",
879             {
880                 completer: function (context) completion.mailFolder(context)
881             });
882
883         // TODO: generate the possible values dynamically from the menu
884         options.add(["layout"],
885             "Set the layout of the mail window",
886             "string", "inherit",
887             {
888                 setter: function (value) {
889                     switch (value) {
890                         case "classic":  ChangeMailLayout(0); break;
891                         case "wide":     ChangeMailLayout(1); break;
892                         case "vertical": ChangeMailLayout(2); break;
893                         // case "inherit" just does nothing
894                     }
895
896                     return value;
897                 },
898                 completer: function (context) [
899                     ["inherit",  "Default View"], // FIXME: correct description?
900                     ["classic",  "Classic View"],
901                     ["wide",     "Wide View"],
902                     ["vertical", "Vertical View"]
903                 ]
904             });
905
906         options.add(["smtpserver", "smtp"],
907             "Set the default SMTP server",
908             "string", services.smtp.defaultServer.key, // TODO: how should we handle these persistent external defaults - "inherit" or null?
909             {
910                 getter: function () services.smtp.defaultServer.key,
911                 setter: function (value) {
912                     let server = mail.smtpServers.filter(function (s) s.key == value)[0];
913                     services.smtp.defaultServer = server;
914                     return value;
915                 },
916                 completer: function (context) [[s.key, s.serverURI] for ([, s] in Iterator(mail.smtpServers))]
917             });
918
919         /*options.add(["threads"],
920             "Use threading to group messages",
921             "boolean", true,
922             {
923                 setter: function (value) {
924                     if (value)
925                         MsgSortThreaded();
926                     else
927                         MsgSortUnthreaded();
928
929                     return value;
930                 }
931             });*/
932     }
933 });
934
935 // vim: set fdm=marker sw=4 ts=4 et: