UNPKG

9.65 kBJavaScriptView Raw
1const CLI = require('clui')
2const Spinner = CLI.Spinner
3const inquirer = require('inquirer')
4const chalk = require('chalk')
5const google = require('googleapis')
6const gmail = google.gmail({ version: 'v1' })
7const btoa = require('btoa')
8const db = require('./db')
9const { parseAndFormatMail, quickMailParse, printHeader } = require('./util')
10const { REPLY, HOME, CREATE } = require('./constants')
11const format = require('./format')
12const welcome = require('./welcome')
13const oauth2Client = require('./auth')
14const {
15 getEmail,
16 getMessagesList,
17 getEmails
18} = require('./client')
19const base64url = require('base64-url')
20const emoji = require('./emoji')
21inquirer.registerPrompt('lazy-list', require('inquirer-plugin-lazy-list'))
22
23class Gmail {
24 constructor (accounts) {
25 this.accounts = accounts
26 this.status = new Spinner('Loading...')
27 this.ui = new inquirer.ui.BottomBar()
28 this.state = {
29 client: undefined,
30 account: {},
31 page: 1,
32 next: undefined,
33 filter: undefined,
34 token: undefined
35 }
36 }
37
38 setState (state) {
39 this.state = {
40 ...this.state,
41 ...state
42 }
43 }
44
45 showAccounts () {
46 console.log(
47 chalk.yellow(
48 JSON.stringify(this.accounts, null, 2)
49 )
50 )
51 this.homeMenu(false)
52 }
53
54 deleteMsg (messageId) {
55 gmail.users.messages.trash({
56 auth: this.state.client,
57 userId: 'me',
58 id: messageId
59 })
60 db.get('messages').remove({id: messageId}).write()
61 }
62
63 sendNew ({ text, sender, recipient, subject }) {
64 this.setState({ stale: true })
65 const base64Encoded = btoa([
66 'Content-Type: text/plain; charset=\"UTF-8\"',
67 'MIME-Version: 1.0',
68 `Subject: ${subject}`,
69 `From: ${sender}`,
70 `To: ${recipient}\n`,
71 `${text}`
72 ].join('\n')).replace(/\+/g, '-').replace(/\//g, '_')
73 gmail.users.messages.send({
74 auth: this.state.client,
75 userId: 'me',
76 resource: { raw: base64Encoded }
77 })
78 }
79
80 send ({ text, sender, recipient, subject, threadId }) {
81 this.setState({ stale: true })
82 const arr = [
83 'Content-Type: text/plain; charset=\"UTF-8\"',
84 'MIME-Version: 1.0',
85 `Subject: ${subject}`,
86 `From: ${sender}`,
87 `To: ${recipient}\n`,
88 `${text}`
89 ]
90 console.log(chalk.cyan(arr.join('\n')))
91 const base64EncodedEmail = btoa(arr.join('\n'))
92 .replace(/\+/g, '-')
93 .replace(/\//g, '_')
94
95 gmail.users.messages.send({
96 auth: this.state.client,
97 userId: 'me',
98 resource: {
99 raw: base64EncodedEmail,
100 threadId
101 }
102 })
103 }
104
105 async replyToMessage (mail, threadId) {
106 this.setState({ stale: true })
107 const answers = await inquirer.prompt([{
108 type: 'input',
109 name: 'subject',
110 message: 'Subject',
111 default: mail && mail.subject ? mail.subject : ''
112 }, {
113 type: 'input',
114 name: 'recipient',
115 message: 'To',
116 default: mail && mail.from && mail.from.value[0].address ? mail.from.value[0].address : ''
117 }, {
118 type: 'input',
119 name: 'draft',
120 message: 'Compose message'
121 }, {
122 type: 'confirm',
123 name: 'confirmation',
124 message: 'Send?'
125 }])
126 if (answers.confirmation) {
127 const text = answers.draft
128 const { recipient, subject } = answers
129 this.send({
130 text,
131 subject,
132 recipient,
133 threadId,
134 sender: this.state.account.emailAddress
135 })
136 console.log(chalk.green('Success!'))
137 }
138 this.configureInboxView()
139 }
140
141 listAccounts (accounts) {
142 return inquirer.prompt([{
143 name: 'accounts',
144 type: 'list',
145 message: 'Accounts',
146 choices: Object.keys(accounts).map(key =>
147 ({ name: key, value: key })
148 )
149 }])
150 }
151
152 async nav ({ source, threadId, messageId }) {
153 const lines = await parseAndFormatMail(source)
154 const mail = await quickMailParse(source)
155
156 this.ui.log.write(
157 lines.join('\n')
158 )
159 const answers = await inquirer.prompt(REPLY)
160 const handlers = {
161 back: () => {
162 this.configureInboxView()
163 },
164 reply: () => {
165 this.replyToMessage(mail, threadId)
166 },
167 delete: () => {
168 this.deleteMsg(messageId)
169 this.configureInboxView()
170 },
171 home: () => {
172 this.homeMenu()
173 },
174 exit: () => {
175 process.exit = 0
176 }
177 }
178 return handlers[answers.nav]()
179 }
180
181 async configureInboxView (type = 'lazy-list') {
182 const now = Date.now()
183 if (
184 !this.state.account.tokens.access_token ||
185 this.state.account.tokens.expiry_date < now
186 ) {
187 console.log(chalk.bold('Please reauthorize this account (under settings).'))
188 return this.homeMenu(false)
189 }
190 let cachedMessagesCount = db.get('messages').value().length
191 welcome(false, cachedMessagesCount)
192 this.status.start()
193 await this.fetchMessages()
194
195 const messages = db.get('messages').value()
196 printHeader(this.state.account.profile, messages)
197
198 const emails = format(messages).map(message => ({
199 value: message.id,
200 name: `${message.headers.subject} (${message.headers.from})`
201 }))
202
203 this.status.stop()
204 const answers = await this.inboxView(emails, type)
205 await this.inboxViewPrompts(answers, messages)
206 }
207
208 async homeMenu (clearfix = true) {
209 let cachedMessagesCount = db.get('messages').value().length
210 welcome(clearfix, cachedMessagesCount)
211 let { home } = await inquirer.prompt(HOME)
212
213 switch (home) {
214 case 'Re-Authorize':
215 require('./server')
216 break
217 case 'Exit':
218 process.exit = 0
219 break
220 case 'Settings':
221 this.showAccounts()
222 break
223 default:
224 let resp = await this.listAccounts(this.accounts)
225 let account = this.accounts[resp.accounts]
226 let client = await oauth2Client()
227 client.setCredentials(account.tokens)
228 this.setState({
229 account,
230 client,
231 token: account.tokens.access_token
232 })
233 this.configureInboxView()
234 }
235 }
236
237 async openEmail (answers, _) {
238 let message = db.get('messages').find(message => message.id === id)
239 let accessToken = this.state.account.tokens.access_token
240 let id = answers.menu
241 let raw = await getEmail({ accessToken, id, format: 'raw' })
242 let source = base64url.decode(raw.raw)
243 this.nav({
244 source,
245 messageId: id,
246 threadId: message.threadId || undefined
247 })
248 }
249
250 async inboxViewPrompts (answers, messages) {
251 const handler = {
252 compose: () => {
253 inquirer.prompt(CREATE).then((answers) => {
254 const { text, subject, recipient } = answers
255 const sender = this.state.account.emailAddress
256 this.sendNew({ subject, recipient, sender, text })
257 }).then(() => { this.configureInboxView() })
258 },
259 checkbox: () => {
260 this.configureInboxView('checkbox')
261 },
262 exit: () => {
263 process.exit = 0
264 },
265 search: () => {
266 inquirer.prompt([{
267 type: 'input',
268 name: 'search',
269 message: 'Search'
270 }]).then((answers) => {
271 this.setState({
272 page: 1,
273 filter: answers.search,
274 next: undefined
275 })
276 this.configureInboxView()
277 })
278 }
279 }
280 if (handler[answers.menu]) {
281 return handler[answers.menu]()
282 }
283 return this.openEmail(answers, messages)
284 }
285
286 async inboxView (choices, type = 'lazy-list') {
287 let options = [
288 new inquirer.Separator(),
289 { name: 'Home', value: 'home' },
290 { name: 'Exit', value: 'exit' },
291 { name: 'Search', value: 'search' },
292 { name: 'Bulk', value: 'bulk' },
293 { name: `Compose ${emoji.message}`, value: 'compose' }
294 ]
295
296 choices.unshift(new inquirer.Separator())
297 choices = choices.concat(options)
298 const fetchMore = async () => {
299 this.state.page++
300 await this.fetchMessages()
301 const messages = db.get('messages').value()
302 const formattedMessages = format(messages)
303 const emails = formattedMessages.map(message => ({
304 value: message.id,
305 name: `${message.headers.subject} (${message.headers.from})`
306 }))
307 return emails
308 }
309
310 let pageSize = 10
311 let inq = inquirer.prompt([{
312 name: 'menu',
313 type,
314 message: 'Inbox',
315 pageSize: pageSize,
316 choices,
317 onChange: (state, eventType) => {
318 let index = state.selected
319 let listLength = state.opt.choices.realLength
320 let shouldFetchMore = (index + pageSize) > listLength
321 if (
322 eventType === 'onDownKey' &&
323 shouldFetchMore
324 ) {
325 return fetchMore(state.opt.choices.length)
326 }
327 }
328 }])
329 return inq
330 }
331
332 async fetchMessages () {
333 const store = db.get('messages')
334 const { token, filter } = this.state
335 const next = db.get(`pages.${this.state.page}`, undefined).value()
336 const resp = await getMessagesList({
337 accessToken: token,
338 next,
339 filter
340 })
341 db.set(`pages.${this.state.page + 1}`, resp.nextPageToken).write()
342 this.setState({ next: resp.nextPageToken })
343 let _messages = resp.messages.filter(message =>
344 !store.find({id: message.id}).value()
345 )
346
347 if (_messages && _messages.length) {
348 const messages = await getEmails({
349 accessToken: token,
350 messages: _messages,
351 format: 'full'
352 })
353 db.set(
354 'messages',
355 messages.concat(store.value()).sort((a, b) =>
356 parseInt(b.internalDate) - parseInt(a.internalDate)
357 )
358 ).write()
359 }
360 }
361}
362
363module.exports = Gmail