vmail 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ require File.expand_path('../../config/environment', __FILE__)
2
+
3
+ require 'minitest/spec'
4
+ require 'minitest/unit'
5
+
6
+ MiniTest::Unit.autorun
7
+
8
+ def read_fixture(name)
9
+ File.read(File.expand_path("../fixtures/#{name}", __FILE__))
10
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+ require 'time'
3
+
4
+ describe "TimeFormat methods" do
5
+ before do
6
+ @time_string = "2010-11-27T06:08:03-05:00"
7
+ @time = Time.parse @time_string
8
+ end
9
+
10
+ it "should convert pacific to eastern" do
11
+ string = "2010-11-27T06:08:03-08:00"
12
+ time = Time.parse(string)
13
+ time.to_s.must_equal "2010-11-27 09:08:03 -0500"
14
+ end
15
+ end
data/viewer.vim ADDED
@@ -0,0 +1,623 @@
1
+ let s:mailbox = ''
2
+ let s:num_msgs = 0 " number of messages
3
+ let s:query = ''
4
+
5
+ let s:drb_uri = $DRB_URI
6
+
7
+ let s:client_script = "vmail_client " . s:drb_uri . " "
8
+ let s:window_width_command = s:client_script . "window_width= "
9
+ let s:list_mailboxes_command = s:client_script . "list_mailboxes "
10
+ let s:lookup_command = s:client_script . "lookup "
11
+ let s:update_command = s:client_script . "update"
12
+ let s:fetch_headers_command = s:client_script . "fetch_headers "
13
+ let s:select_mailbox_command = s:client_script . "select_mailbox "
14
+ let s:search_command = s:client_script . "search "
15
+ let s:more_messages_command = s:client_script . "more_messages "
16
+ let s:flag_command = s:client_script . "flag "
17
+ let s:move_to_command = s:client_script . "move_to "
18
+ let s:new_message_template_command = s:client_script . "new_message_template "
19
+ let s:reply_template_command = s:client_script . "reply_template "
20
+ let s:forward_template_command = s:client_script . "forward_template "
21
+ let s:deliver_command = s:client_script . "deliver "
22
+ let s:save_draft_command = s:client_script . "save_draft "
23
+ let s:save_attachments_command = s:client_script . "save_attachments "
24
+ let s:open_html_command = s:client_script . "open_html_part "
25
+ let s:message_bufname = "MessageWindow"
26
+ let s:list_bufname = "MessageListWindow"
27
+
28
+ function! VmailStatusLine()
29
+ return "%<%f\ " . s:mailbox . "%r%=%-14.(%l,%c%V%)\ %P"
30
+ endfunction
31
+
32
+ function! s:create_list_window()
33
+ "setlocal bufhidden=delete
34
+ "setlocal buftype=nofile
35
+ setlocal nomodifiable
36
+ setlocal noswapfile
37
+ "setlocal nomodifiable
38
+ setlocal nowrap
39
+ setlocal nonumber
40
+ setlocal foldcolumn=0
41
+ setlocal nospell
42
+ " setlocal nobuflisted
43
+ setlocal textwidth=0
44
+ setlocal noreadonly
45
+ " hi CursorLine cterm=NONE ctermbg=darkred ctermfg=white guibg=darkred guifg=white
46
+ setlocal cursorline
47
+ " we need the bufnr to find the window later
48
+ let s:listbufnr = bufnr('%')
49
+ setlocal statusline=%!VmailStatusLine()
50
+ endfunction
51
+
52
+ " the message display buffer window
53
+ function! s:create_message_window()
54
+ exec "split " . s:message_bufname
55
+ setlocal buftype=nofile
56
+ " setlocal noswapfile
57
+ " setlocal nobuflisted
58
+ let s:message_window_bufnr = bufnr('%')
59
+ " message window bindings
60
+ noremap <silent> <buffer> <cr> :call <SID>focus_list_window()<CR>
61
+ noremap <silent> <buffer> <Leader>q :call <SID>focus_list_window()<CR>
62
+ noremap <silent> <buffer> q <Leader>q
63
+ noremap <silent> <buffer> <Leader>r :call <SID>compose_reply(0)<CR>
64
+ noremap <silent> <buffer> <Leader>a :call <SID>compose_reply(1)<CR>
65
+ noremap <silent> <buffer> <Leader>R :call <SID>show_raw()<cr>
66
+ noremap <silent> <buffer> <Leader>R :call <SID>show_raw()<cr>
67
+ noremap <silent> <buffer> <Leader>f :call <SID>compose_forward()<CR><cr>
68
+ " TODO improve this
69
+ noremap <silent> <buffer> <Leader>o yE :!open '<C-R>"'<CR><CR>
70
+ noremap <silent> <buffer> <leader>j :call <SID>show_next_message()<CR>
71
+ noremap <silent> <buffer> <leader>k :call <SID>show_previous_message()<CR>
72
+ noremap <silent> <buffer> <Leader>c :call <SID>compose_message()<CR>
73
+ noremap <silent> <buffer> <Leader>h :call <SID>open_html_part()<CR><cr>
74
+ nnoremap <silent> <buffer> q :close<cr>
75
+ nnoremap <silent> <buffer> <leader>d :call <SID>focus_list_window()<cr>:call <SID>delete_messages("Deleted")<cr>
76
+ nnoremap <silent> <buffer> s :call <SID>focus_list_window()<cr>:call <SID>toggle_star()<cr>
77
+ nnoremap <silent> <buffer> <Leader>m :call <SID>focus_list_window()<cr>:call <SID>mailbox_window()<CR>
78
+ nnoremap <silent> <buffer> <Leader>A :call <SID>save_attachments()<cr>
79
+ " go fullscreen
80
+ nnoremap <silent> <buffer> <Space> :call <SID>toggle_fullscreen()<cr>
81
+ close
82
+ endfunction
83
+
84
+ function! s:show_message()
85
+ let line = getline(line("."))
86
+ if match(line, '^> Load') != -1
87
+ setlocal modifiable
88
+ delete
89
+ call s:more_messages()
90
+ return
91
+ endif
92
+ " remove the unread flag [+]
93
+ let newline = substitute(line, "\\[+\]\\s*", "", '')
94
+ setlocal modifiable
95
+ call setline(line('.'), newline)
96
+ setlocal nomodifiable
97
+ write
98
+ " this just clears the command line and prevents the screen from
99
+ " moving up when the next echo statement executes:
100
+ call feedkeys(":\<cr>")
101
+ redraw
102
+ let selected_uid = matchstr(line, '^\d\+')
103
+ let s:current_uid = selected_uid
104
+ let command = s:lookup_command . s:current_uid
105
+ echo "Loading message. Please wait..."
106
+ let res = system(command)
107
+ call s:focus_message_window()
108
+ setlocal modifiable
109
+ 1,$delete
110
+ put =res
111
+ " critical: don't call execute 'normal \<cr>'
112
+ " call feedkeys("<cr>")
113
+ 1delete
114
+ normal 1
115
+ normal jk
116
+ setlocal nomodifiable
117
+ redraw
118
+ endfunction
119
+
120
+ function! s:show_next_message()
121
+ call s:focus_list_window()
122
+ execute "normal j"
123
+ execute "normal \<cr>"
124
+ endfunction
125
+
126
+ function! s:show_previous_message()
127
+ call s:focus_list_window()
128
+ execute "normal k"
129
+ if line('.') != 1
130
+ execute "normal \<cr>"
131
+ endif
132
+ endfunction
133
+
134
+ " invoked from withint message window
135
+ function! s:show_raw()
136
+ let command = s:lookup_command . s:current_uid . ' raw'
137
+ echo command
138
+ setlocal modifiable
139
+ 1,$delete
140
+ let res = system(command)
141
+ put =res
142
+ 1delete
143
+ normal 1G
144
+ setlocal nomodifiable
145
+ endfunction
146
+
147
+
148
+ function! s:focus_list_window()
149
+ let winnr = bufwinnr(s:listbufnr)
150
+ if winnr == -1
151
+ " create window
152
+ split
153
+ exec "buffer" . s:listbufnr
154
+ else
155
+ exec winnr . "wincmd w"
156
+ endif
157
+ " set up syntax highlighting
158
+ if has("syntax")
159
+ syn clear
160
+ syn match BufferFlagged /^.*[*].*$/hs=s
161
+ hi def BufferFlagged ctermfg=red ctermbg=black
162
+ endif
163
+ " vertically center the cursor line
164
+ normal z.
165
+ call feedkeys("\<c-l>") " prevents screen artifacts when user presses return too fast
166
+ endfunction
167
+
168
+ function! s:focus_message_window()
169
+ let winnr = bufwinnr(s:message_window_bufnr)
170
+ if winnr == -1
171
+ " create window
172
+ exec "botright split " . s:message_bufname
173
+ else
174
+ exec winnr . "wincmd w"
175
+ endif
176
+ endfunction
177
+
178
+ " gets new messages since last update
179
+ function! s:update()
180
+ let command = s:update_command
181
+ echo command
182
+ let res = system(command)
183
+ if match(res, '^\d\+') != -1
184
+ setlocal modifiable
185
+ let line = line('$')
186
+ $put =res
187
+ setlocal nomodifiable
188
+ let num = len(split(res, '\n', ''))
189
+ redraw
190
+ call cursor(line + 1, 0)
191
+ normal z.
192
+ redraw
193
+ echo "you have " . num . " new message" . (num == 1 ? '' : 's') . "!"
194
+ else
195
+ redraw
196
+ echo "no new messages"
197
+ endif
198
+ endfunction
199
+
200
+ function! s:toggle_star() range
201
+ let lnum = a:firstline
202
+ let n = 0
203
+ let uids = []
204
+ while lnum <= a:lastline
205
+ let line = getline(lnum)
206
+ let message_uid = matchstr(line, '^\d\+')
207
+ call add(uids, message_uid)
208
+ let lnum = lnum + 1
209
+ endwhile
210
+ let uid_set = join(uids, ",")
211
+ let flag_symbol = "[*]"
212
+ " check if starred already
213
+ let action = " +FLAGS"
214
+ if (match(line, flag_symbol) != -1)
215
+ let action = " -FLAGS"
216
+ endif
217
+ let command = s:flag_command . uid_set . action . " Flagged"
218
+ echo command
219
+ " toggle [*] on lines
220
+ let res = system(command)
221
+ setlocal modifiable
222
+ exec a:firstline . "," . a:lastline . "delete"
223
+ exec (a:firstline - 1). "put =res"
224
+ setlocal nomodifiable
225
+ " if more than 2 lines change, vim forces us to look at a message.
226
+ " dismiss it.
227
+ if len(split(res, "\n")) > 2
228
+ call feedkeys("\<cr>")
229
+ endif
230
+ endfunction
231
+
232
+ " flag can be Deleted or [Gmail]/Spam
233
+ func! s:delete_messages(flag) range
234
+ let lnum = a:firstline
235
+ let n = 0
236
+ let uids = []
237
+ while lnum <= a:lastline
238
+ let line = getline(lnum)
239
+ let message_uid = matchstr(line, '^\d\+')
240
+ call add(uids, message_uid)
241
+ let lnum = lnum + 1
242
+ endwhile
243
+ let uid_set = join(uids, ",")
244
+ let command = s:flag_command . uid_set . " +FLAGS " . a:flag
245
+ echo command
246
+ let res = system(command)
247
+ setlocal modifiable
248
+ exec a:firstline . "," . a:lastline . "delete"
249
+ setlocal nomodifiable
250
+ " if more than 2 lines change, vim forces us to look at a message.
251
+ " dismiss it.
252
+ if len(uids) > 2
253
+ call feedkeys("\<cr>")
254
+ endif
255
+ endfunc
256
+
257
+ " --------------------------------------------------------------------------------
258
+ " move to another mailbox
259
+ function! s:move_to_mailbox() range
260
+ let lnum = a:firstline
261
+ let n = 0
262
+ let uids = []
263
+ while lnum <= a:lastline
264
+ let line = getline(lnum)
265
+ let message_uid = matchstr(line, '^\d\+')
266
+ call add(uids, message_uid)
267
+ let lnum = lnum + 1
268
+ endwhile
269
+ let s:uid_set = join(uids, ",")
270
+ " now prompt use to select mailbox
271
+ if !exists("s:mailboxes")
272
+ call s:get_mailbox_list()
273
+ endif
274
+ topleft split MailboxSelect
275
+ setlocal buftype=nofile
276
+ setlocal noswapfile
277
+ setlocal modifiable
278
+ resize 1
279
+ inoremap <silent> <buffer> <cr> <Esc>:call <SID>complete_move_to_mailbox()<CR>
280
+ inoremap <silent> <buffer> <esc> <Esc>:q<cr>
281
+ set completefunc=CompleteMoveMailbox
282
+ " c-p clears the line
283
+ let s:firstline = a:firstline
284
+ let s:lastline = a:lastline
285
+ call feedkeys("i\<c-x>\<c-u>\<c-p>", 't')
286
+ " save these in script scope to delete the lines when move completes
287
+ endfunction
288
+
289
+ " Open command window to choose a mailbox to move a message to.
290
+ " Very similar to mailbox_window() function
291
+ function! s:complete_move_to_mailbox()
292
+ let mailbox = getline(line('.'))
293
+ close
294
+ " check if mailbox is a real mailbox
295
+ if (index(s:mailboxes, mailbox) == -1)
296
+ return
297
+ endif
298
+ let command = s:move_to_command . s:uid_set . ' ' . shellescape(mailbox)
299
+ echo command
300
+ let res = system(command)
301
+ setlocal modifiable
302
+ exec s:firstline . "," . s:lastline . "delete"
303
+ setlocal nomodifiable
304
+ endfunction
305
+
306
+ function! CompleteMoveMailbox(findstart, base)
307
+ if !exists("s:mailboxes")
308
+ call s:get_mailbox_list()
309
+ endif
310
+ if a:findstart
311
+ " locate the start of the word
312
+ return 0
313
+ else
314
+ " find months matching with "a:base"
315
+ let res = []
316
+ for m in s:mailboxes
317
+ if m == s:mailbox
318
+ continue
319
+ endif
320
+ if m =~ '^' . a:base
321
+ call add(res, m)
322
+ endif
323
+ endfor
324
+ return res
325
+ endif
326
+ endfun
327
+ " --------------------------------------------------------------------------------
328
+
329
+ function! s:get_mailbox_list()
330
+ let command = s:list_mailboxes_command
331
+ redraw
332
+ echo command
333
+ let res = system(command)
334
+ let s:mailboxes = split(res, "\n", '')
335
+ endfunction
336
+
337
+ function! CompleteMailbox(findstart, base)
338
+ if !exists("s:mailboxes")
339
+ call s:get_mailbox_list()
340
+ endif
341
+ if a:findstart
342
+ " locate the start of the word
343
+ return 0
344
+ else
345
+ " find mailboxes matching with "a:base"
346
+ let res = []
347
+ for m in s:mailboxes
348
+ if m =~ '^' . a:base
349
+ call add(res, m)
350
+ endif
351
+ endfor
352
+ return res
353
+ endif
354
+ endfun
355
+
356
+ " -------------------------------------------------------------------------------
357
+ " select mailbox
358
+
359
+ function! s:mailbox_window()
360
+ if !exists("s:mailboxes")
361
+ call s:get_mailbox_list()
362
+ endif
363
+ topleft split MailboxSelect
364
+ setlocal buftype=nofile
365
+ setlocal noswapfile
366
+ setlocal modifiable
367
+ resize 1
368
+ inoremap <silent> <buffer> <cr> <Esc>:call <SID>select_mailbox()<CR>
369
+ inoremap <silent> <buffer> <esc> <Esc>:q<cr>
370
+ set completefunc=CompleteMailbox
371
+ " c-p clears the line
372
+ call feedkeys("i\<c-x>\<c-u>\<c-p>", 't')
373
+ endfunction
374
+
375
+ function! s:select_mailbox()
376
+ let mailbox = getline(line('.'))
377
+ close
378
+ call s:focus_message_window()
379
+ close
380
+ " check if mailbox is a real mailbox
381
+ if (index(s:mailboxes, mailbox) == -1)
382
+ return
383
+ endif
384
+ let s:mailbox = mailbox
385
+ let command = s:select_mailbox_command . shellescape(s:mailbox)
386
+ echo command
387
+ call system(command)
388
+ redraw
389
+ " now get latest 100 messages
390
+ call s:focus_list_window()
391
+ setlocal modifiable
392
+ let command = s:search_command . "100 all"
393
+ echo "Please wait. Loading messages..."
394
+ let res = system(command)
395
+ 1,$delete
396
+ put! =res
397
+ execute "normal Gdd\<c-y>"
398
+ normal G
399
+ setlocal nomodifiable
400
+ normal z.
401
+ endfunction
402
+
403
+ function! s:search_window()
404
+ topleft split SearchWindow
405
+ setlocal buftype=nofile
406
+ setlocal noswapfile
407
+ resize 1
408
+ setlocal modifiable
409
+ noremap! <silent> <buffer> <cr> <Esc>:call <SID>do_search()<CR>
410
+ call feedkeys("i")
411
+ endfunction
412
+
413
+ function! s:do_search()
414
+ let s:query = getline(line('.'))
415
+ close
416
+ " empty query
417
+ if match(s:query, '^\s*$') != -1
418
+ return
419
+ endif
420
+ " close message window if open
421
+ call s:focus_message_window()
422
+ close
423
+ " TODO should we really hardcode 100 as the quantity?
424
+ let command = s:search_command . "100 " . shellescape(s:query)
425
+ echo command
426
+ call s:focus_list_window()
427
+ let res = system(command)
428
+ setlocal modifiable
429
+ 1,$delete
430
+ put! =res
431
+ execute "normal Gdd\<c-y>"
432
+ normal z.
433
+ setlocal nomodifiable
434
+ endfunction
435
+
436
+ function! s:more_messages()
437
+ let line = getline(line('.'))
438
+ let uid = matchstr(line, '^\d\+')
439
+ let command = s:more_messages_command . uid
440
+ echo command
441
+ let res = system(command)
442
+ setlocal modifiable
443
+ let lines = split(res, "\n")
444
+ call append(0, lines)
445
+ " execute "normal Gdd\<c-y>"
446
+ setlocal nomodifiable
447
+
448
+ endfunction
449
+
450
+ " --------------------------------------------------------------------------------
451
+ " compose reply, compose, forward, save draft
452
+
453
+ function! s:compose_reply(all)
454
+ let command = s:reply_template_command . s:current_uid
455
+ if a:all
456
+ let command = command . ' 1'
457
+ endif
458
+ call s:open_compose_window(command)
459
+ " cursor after headers
460
+ normal }
461
+ normal o
462
+ endfunction
463
+
464
+ function! s:compose_message()
465
+ write
466
+ let command = s:new_message_template_command
467
+ call s:open_compose_window(command)
468
+ " position cursor after to:
469
+ call search("^to:")
470
+ normal $
471
+ call feedkeys("a")
472
+ endfunction
473
+
474
+ function! s:compose_forward()
475
+ write
476
+ let command = s:forward_template_command . s:current_uid
477
+ call s:open_compose_window(command)
478
+ call search("^to:")
479
+ normal $
480
+ call feedkeys("a")
481
+ endfunction
482
+
483
+ func! s:open_compose_window(command)
484
+ redraw
485
+ echo a:command
486
+ let res = system(a:command)
487
+ split ComposeMessage
488
+ wincmd p
489
+ close
490
+ setlocal modifiable
491
+ 1,$delete
492
+ put! =res
493
+ normal 1G
494
+ noremap <silent> <buffer> <Leader>d :call <SID>deliver_message()<CR>
495
+ nnoremap <silent> <buffer> q :call <SID>cancel_compose()<cr>
496
+ nnoremap <silent> <buffer> <leader>q :call <SID>cancel_compose()<cr>
497
+ nnoremap <silent> <buffer> <Leader>s :call <SID>save_draft()<CR>
498
+ set completefunc=CompleteContact
499
+ endfunc
500
+
501
+ " contacts.txt file should be generated.
502
+ " grep works well, does partial matches
503
+ function! CompleteContact(findstart, base)
504
+ if !exists("s:mailboxes")
505
+ call s:get_mailbox_list()
506
+ endif
507
+ if a:findstart
508
+ " locate the start of the word
509
+ let line = getline('.')
510
+ let start = col('.') - 1
511
+ while start > 0 && line[start - 1] =~ '\a'
512
+ let start -= 1
513
+ endwhile
514
+ return start
515
+ else
516
+ " find contacts matching with "a:base"
517
+ let matches = system("grep " . shellescape(a:base) . " contacts.txt")
518
+ return split(matches, "\n")
519
+ endif
520
+ endfun
521
+
522
+ function! s:cancel_compose()
523
+ call s:focus_list_window()
524
+ wincmd p
525
+ close!
526
+ endfunction
527
+
528
+ function! s:deliver_message()
529
+ write
530
+ let mail = join(getline(1,'$'), "\n")
531
+ exec ":!" . s:deliver_command . " < ComposeMessage"
532
+ redraw
533
+ call s:focus_list_window()
534
+ wincmd p
535
+ close!
536
+ endfunction
537
+
538
+ func! s:save_draft()
539
+ write
540
+ let mail = join(getline(1,'$'), "\n")
541
+ exec ":!" . s:save_draft_command . " < ComposeMessage"
542
+ redraw
543
+ call s:focus_list_window()
544
+ wincmd p
545
+ close!
546
+ endfunc
547
+
548
+ " --------------------------------------------------------------------------------
549
+
550
+ " call from inside message window with <Leader>h
551
+ func! s:open_html_part()
552
+ let command = s:open_html_command . s:current_uid
553
+ let outfile = system(command)
554
+ " todo: allow user to change open in browser command?
555
+ exec "!open " . outfile
556
+ endfunc
557
+
558
+ func! s:save_attachments()
559
+ if !exists("s:savedir")
560
+ let s:savedir = getcwd() . "/attachments"
561
+ end
562
+ let s:savedir = input("save attachments to directory: ", s:savedir)
563
+ let command = s:save_attachments_command . s:savedir
564
+ echo command
565
+ let res = system(command)
566
+ endfunc
567
+ " --------------------------------------------------------------------------------
568
+
569
+ func! s:toggle_fullscreen()
570
+ if winnr('$') > 1
571
+ only
572
+ normal z.
573
+ else
574
+ call feedkeys("\<cr>")
575
+ endif
576
+ endfunc
577
+
578
+ call s:create_list_window()
579
+
580
+ call s:create_message_window()
581
+
582
+ call s:focus_list_window() " to go list window
583
+ " this are list window bindings
584
+
585
+ noremap <silent> <buffer> <cr> :call <SID>show_message()<CR>
586
+ noremap <silent> <buffer> q :qal!<cr>
587
+
588
+ noremap <silent> <buffer> s :call <SID>toggle_star()<CR>
589
+ noremap <silent> <buffer> <leader>d :call <SID>delete_messages("Deleted")<CR>
590
+
591
+ " TODO the range doesn't quite work as expect, need <line1> <line2>
592
+ " trying to make user defined commands that work from : prompt
593
+ " command -buffer -range VmailDelete call s:toggle_star("Deleted")
594
+ " command -buffer -range VmailStar call s:toggle_star("Flagged")
595
+
596
+ noremap <silent> <buffer> <leader>! :call <SID>delete_messages("[Gmail]/Spam")<CR>
597
+
598
+ "open a link browser (os x)
599
+ "autocmd CursorMoved <buffer> call <SID>show_message()
600
+
601
+ noremap <silent> <buffer> u :call <SID>update()<CR>
602
+ noremap <silent> <buffer> <Leader>s :call <SID>search_window()<CR>
603
+ noremap <silent> <buffer> <Leader>m :call <SID>mailbox_window()<CR>
604
+ noremap <silent> <buffer> <Leader>v :call <SID>move_to_mailbox()<CR>
605
+
606
+ noremap <silent> <buffer> <Leader>c :call <SID>compose_message()<CR>
607
+
608
+ noremap <silent> <buffer> <Leader>r :call <SID>show_message()<cr>:call <SID>compose_reply(0)<CR>
609
+ noremap <silent> <buffer> <Leader>a :call <SID>show_message()<cr>:call <SID>compose_reply(1)<CR>
610
+
611
+ " go fullscreen
612
+ nnoremap <silent> <buffer> <Space> :call <SID>toggle_fullscreen()<cr>
613
+
614
+ " press double return in list view to go full screen on a message; then
615
+ " return? again to restore the list view
616
+
617
+ " go to bottom and center cursorline
618
+ normal G
619
+ normal z.
620
+
621
+ " send window width
622
+ " system(s:set_window_width_command . winwidth(1))
623
+