├── README.md ├── focus-and-reply.js ├── index.html └── style.css /README.md: -------------------------------------------------------------------------------- 1 | # focus-reply-fastmail 2 | a hacky focus & reply feature for fastmail 3 | -------------------------------------------------------------------------------- /focus-and-reply.js: -------------------------------------------------------------------------------- 1 | function restore() { 2 | let auth = JSON.parse(localStorage.getItem('auth')); 3 | app.username = auth.username; 4 | app.password = auth.password; 5 | } 6 | 7 | function make_jmap_query(x) { 8 | return { 9 | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], 10 | "methodCalls": x 11 | } 12 | } 13 | 14 | async function get_mailbox_id(accountId) { 15 | let mbox_query = make_jmap_query([[ "Mailbox/get", { 16 | "accountId": accountId, 17 | "ids": null 18 | }, "0" ]]); 19 | let data = await (await fetch('https://jmap.fastmail.com/api/', { 20 | method: 'POST', 21 | headers: { 22 | "Content-Type": "application/json", 23 | "Authorization": "Basic " + authBasic() 24 | }, 25 | body: JSON.stringify(mbox_query) 26 | })).json(); 27 | for (mailbox of data.methodResponses[0][1]['list']) { 28 | if (mailbox.name.toLowerCase() === "reply later") { 29 | return mailbox.id; 30 | } 31 | } 32 | } 33 | 34 | async function emails_query(accountId) { 35 | 36 | let mailbox_id = await get_mailbox_id(accountId); 37 | return [[ "Email/query", { 38 | "accountId": accountId, 39 | // todo: actually do the reply later thing 40 | "filter": { "inMailbox": mailbox_id }, 41 | "sort": [{ "property": "receivedAt", "isAscending": false }], 42 | "collapseThreads": true, 43 | "position": 0, 44 | "limit": 20, 45 | "calculateTotal": true 46 | }, "t0" ], 47 | [ "Email/get", { 48 | "accountId": accountId, 49 | "#ids": { 50 | "resultOf": "t0", 51 | "name": "Email/query", 52 | "path": "/ids" 53 | }, 54 | "properties": [ "threadId" ] 55 | }, "t1" ], 56 | [ "Thread/get", { 57 | "accountId": accountId, 58 | "#ids": { 59 | "resultOf": "t1", 60 | "name": "Email/get", 61 | "path": "/list/*/threadId" 62 | } 63 | }, "t2" ], 64 | [ "Email/get", { 65 | "accountId": accountId, 66 | "fetchTextBodyValues": true, 67 | "#ids": { 68 | "resultOf": "t2", 69 | "name": "Thread/get", 70 | "path": "/list/*/emailIds" 71 | }, 72 | "properties": [ "from", "receivedAt", "subject", "bodyValues", "threadId"] 73 | }, "t3" ]] 74 | } 75 | 76 | function groupEmails(emails) { 77 | let threads = emails.reduce((groups, item) => ({ 78 | ...groups, 79 | [item.threadId]: [...(groups[item.threadId] || []), item] 80 | }), {}); 81 | 82 | let ret = []; 83 | for (id in threads) { 84 | let thread = threads[id]; 85 | let last = thread[thread.length - 1]; 86 | ret.push(last); 87 | } 88 | return ret; 89 | } 90 | 91 | function authBasic() { 92 | 93 | return window.btoa(app.username + ':' + app.password); 94 | } 95 | 96 | async function get_emails() { 97 | localStorage.setItem('auth', JSON.stringify({ 98 | username: app.username, 99 | password: app.password, 100 | })); 101 | let session = await (await fetch('https://jmap.fastmail.com/session', { 102 | headers: { 103 | "Authorization": "Basic " + authBasic() 104 | } 105 | })).json(); 106 | let accountId = session['primaryAccounts']['urn:ietf:params:jmap:mail']; 107 | let query = make_jmap_query(await emails_query(accountId)); 108 | 109 | let data = await (await fetch('https://jmap.fastmail.com/api/', { 110 | method: 'POST', 111 | headers: { 112 | "Content-Type": "application/json", 113 | "Authorization": "Basic " + authBasic() 114 | }, 115 | body: JSON.stringify(query) 116 | })).json(); 117 | if (data.methodResponses[0][0] === 'error') { 118 | return 119 | } 120 | app.loggedIn = true; 121 | let emails = data['methodResponses'][3][1]['list']; 122 | app.emails = groupEmails(emails); 123 | } 124 | 125 | async function login() { 126 | app.loading = true; 127 | get_emails().then(() => { 128 | app.loading = false; 129 | setTimeout(loadTextAreas, 100); 130 | }).catch(err => { 131 | console.log(err); 132 | app.loading = false; 133 | }); 134 | } 135 | 136 | var app = new Vue({ 137 | el: '#app', 138 | data: { 139 | message: 'Hello Vue!', 140 | username: null, 141 | password: null, 142 | loading: false, 143 | loggedIn: false, 144 | emails: [] 145 | }, 146 | methods: { 147 | login: login, 148 | logout: function() { 149 | app.username = null; 150 | app.password = null; 151 | app.loggedIn = false; 152 | localStorage.clear(); 153 | }, 154 | fix_email: function(email) { 155 | return Object.values(email.bodyValues)[0].value; 156 | }, 157 | } 158 | }) 159 | restore(); 160 | function saveTextAreas() { 161 | let drafts = {}; 162 | for (x of document.querySelectorAll('textarea')) { 163 | if (x.value) { 164 | drafts[x.id] = x.value; 165 | } 166 | } 167 | if (Object.keys(drafts).length > 0) { 168 | localStorage.setItem('drafts', JSON.stringify(drafts)); 169 | } 170 | } 171 | function loadTextAreas() { 172 | let drafts = JSON.parse(localStorage.getItem('drafts') || '{}'); 173 | for (x of document.querySelectorAll('textarea')) { 174 | let saved = drafts[x.id]; 175 | if (saved) { 176 | x.value = saved; 177 | } 178 | } 179 | } 180 | setInterval(saveTextAreas, 1000); 181 | 182 | if(app.username && app.password) { 183 | login(); 184 | } 185 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | . 5 |{{fix_email(email)}}26 |
49 | All your authentication data is stored in your browser's local 50 | storage, this app has no server code at all. 51 |
52 |