Merge lp:~mardy/account-polld/dekko-gmail into lp:~ubuntu-push-hackers/account-polld/trunk

Proposed by Alberto Mardegan
Status: Merged
Approved by: Jonas G. Drange
Approved revision: 170
Merged at revision: 174
Proposed branch: lp:~mardy/account-polld/dekko-gmail
Merge into: lp:~ubuntu-push-hackers/account-polld/trunk
Prerequisite: lp:~mardy/account-polld/skip-unsupported
Diff against target: 521 lines (+479/-0)
3 files modified
cmd/account-polld/main.go (+6/-0)
plugins/dekko/api.go (+127/-0)
plugins/dekko/dekko.go (+346/-0)
To merge this branch: bzr merge lp:~mardy/account-polld/dekko-gmail
Reviewer Review Type Date Requested Status
Jonas G. Drange (community) Approve
system-apps-ci-bot continuous-integration Approve
Review via email: mp+300873@code.launchpad.net

Commit message

Add Dekko GMail plugin

Description of the change

Add Dekko GMail plugin

To post a comment you must log in.
Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :

PASSED: Continuous integration, rev:170
https://jenkins.canonical.com/system-apps/job/lp-account-polld-ci/15/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build/1032
    SUCCESS: https://jenkins.canonical.com/system-apps/job/test-0-autopkgtest/label=phone-armhf,release=vivid+overlay,testname=default/200
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-0-fetch/1032
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-1-sourcepkg/release=vivid+overlay/929
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-1-sourcepkg/release=xenial+overlay/929
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-1-sourcepkg/release=yakkety/929
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=vivid+overlay/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=vivid+overlay/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=yakkety/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=yakkety/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=vivid+overlay/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=vivid+overlay/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=yakkety/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=yakkety/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=vivid+overlay/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=vivid+overlay/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/923/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=yakkety/923
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=yakkety/923/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/system-apps/job/lp-account-polld-ci/15/rebuild

review: Approve (continuous-integration)
Revision history for this message
Jonas G. Drange (jonas-drange) wrote :

Everything looks okay, except for the duplication of some code from the gmail plugin. Have you looked at the possibility of sharing some of the code?

review: Needs Information
Revision history for this message
Alberto Mardegan (mardy) wrote :

> Everything looks okay, except for the duplication of some code from the gmail
> plugin. Have you looked at the possibility of sharing some of the code?

I haven't. :-) The reason is that the plan is to extend account-polld to support out-of-process plugins, after which we could reuse Dekko's own IMAP/POP3 client implementation and drop this plugin altogether.
You can read more about this in bug 1421923, from comment #18 on.

Revision history for this message
Jonas G. Drange (jonas-drange) wrote :

Then let's not invest too much time and energy in introducing some sort of code sharing between plugins—that may benefit from being completely separate packages anyway.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/account-polld/main.go'
2--- cmd/account-polld/main.go 2016-07-22 11:44:01 +0000
3+++ cmd/account-polld/main.go 2016-07-22 11:44:01 +0000
4@@ -27,6 +27,7 @@
5 "launchpad.net/account-polld/accounts"
6 "launchpad.net/account-polld/gettext"
7 "launchpad.net/account-polld/plugins"
8+ "launchpad.net/account-polld/plugins/dekko"
9 "launchpad.net/account-polld/plugins/gcalendar"
10 "launchpad.net/account-polld/plugins/gmail"
11 "launchpad.net/account-polld/plugins/twitter"
12@@ -48,6 +49,7 @@
13 /* Use identifiers and API keys provided by the respective webapps which are the official
14 end points for the notifications */
15 const (
16+ SERVICENAME_DEKKO = "dekko.dekkoproject_dekko"
17 SERVICENAME_GMAIL = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail"
18 SERVICENAME_TWITTER = "com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter"
19 SERVICENAME_GCALENDAR = "google-caldav"
20@@ -99,6 +101,7 @@
21
22 func monitorAccounts(postWatch chan *PostWatch, pollBus *pollbus.PollBus) {
23 watcher := accounts.NewWatcher()
24+ watcher.AddService(SERVICENAME_DEKKO)
25 watcher.AddService(SERVICENAME_GMAIL)
26 watcher.AddService(SERVICENAME_GCALENDAR)
27 watcher.AddService(SERVICENAME_TWITTER)
28@@ -131,6 +134,9 @@
29 var plugin plugins.Plugin
30 log.Println("Creating plugin for service: ", data.ServiceName)
31 switch data.ServiceName {
32+ case SERVICENAME_DEKKO:
33+ log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
34+ plugin = dekko.New(data.AccountId)
35 case SERVICENAME_GMAIL:
36 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
37 plugin = gmail.New(data.AccountId)
38
39=== added directory 'plugins/dekko'
40=== added file 'plugins/dekko/api.go'
41--- plugins/dekko/api.go 1970-01-01 00:00:00 +0000
42+++ plugins/dekko/api.go 2016-07-22 11:44:01 +0000
43@@ -0,0 +1,127 @@
44+/*
45+ Copyright 2014 Canonical Ltd.
46+
47+ This program is free software: you can redistribute it and/or modify it
48+ under the terms of the GNU General Public License version 3, as published
49+ by the Free Software Foundation.
50+
51+ This program is distributed in the hope that it will be useful, but
52+ WITHOUT ANY WARRANTY; without even the implied warranties of
53+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
54+ PURPOSE. See the GNU General Public License for more details.
55+
56+ You should have received a copy of the GNU General Public License along
57+ with this program. If not, see <http://www.gnu.org/licenses/>.
58+*/
59+
60+package dekko
61+
62+import (
63+ "fmt"
64+ "time"
65+
66+ "launchpad.net/account-polld/plugins"
67+)
68+
69+const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700"
70+
71+type pushes map[string]*plugins.PushMessage
72+type headers map[string]string
73+
74+// messageList holds a response to call to Users.messages: list
75+// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list
76+type messageList struct {
77+ // Messages holds a list of message.
78+ Messages []message `json:"messages"`
79+ // NextPageToken is used to retrieve the next page of results in the list.
80+ NextPageToken string `json:"nextPageToken"`
81+ // ResultSizeEstimage is the estimated total number of results.
82+ ResultSizeEstimage uint64 `json:"resultSizeEstimate"`
83+}
84+
85+// message holds a partial response for a Users.messages.
86+// The full definition of a message is defined in
87+// https://developers.google.com/gmail/api/v1/reference/users/messages#resource
88+type message struct {
89+ // Id is the immutable ID of the message.
90+ Id string `json:"id"`
91+ // ThreadId is the ID of the thread the message belongs to.
92+ ThreadId string `json:"threadId"`
93+ // HistoryId is the ID of the last history record that modified
94+ // this message.
95+ HistoryId string `json:"historyId"`
96+ // Snippet is a short part of the message text. This text is
97+ // used for the push message summary.
98+ Snippet string `json:"snippet"`
99+ // Payload represents the message payload.
100+ Payload payload `json:"payload"`
101+}
102+
103+func (m message) String() string {
104+ return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10])
105+}
106+
107+// ById implements sort.Interface for []message based on
108+// the Id field.
109+type byId []message
110+
111+func (m byId) Len() int { return len(m) }
112+func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
113+func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id }
114+
115+// payload represents the message payload.
116+type payload struct {
117+ Headers []messageHeader `json:"headers"`
118+}
119+
120+func (p *payload) mapHeaders() headers {
121+ headers := make(map[string]string)
122+ for _, hdr := range p.Headers {
123+ headers[hdr.Name] = hdr.Value
124+ }
125+ return headers
126+}
127+
128+func (hdr headers) getTimestamp() time.Time {
129+ timestamp, ok := hdr[hdrDATE]
130+ if !ok {
131+ return time.Now()
132+ }
133+
134+ if t, err := time.Parse(gmailTime, timestamp); err == nil {
135+ return t
136+ }
137+ return time.Now()
138+}
139+
140+func (hdr headers) getEpoch() int64 {
141+ return hdr.getTimestamp().Unix()
142+}
143+
144+// messageHeader represents the message headers.
145+type messageHeader struct {
146+ Name string `json:"name"`
147+ Value string `json:"value"`
148+}
149+
150+type errorResp struct {
151+ Err struct {
152+ Code uint64 `json:"code"`
153+ Message string `json:"message"`
154+ Errors []struct {
155+ Domain string `json:"domain"`
156+ Reason string `json:"reason"`
157+ Message string `json:"message"`
158+ } `json:"errors"`
159+ } `json:"error"`
160+}
161+
162+func (err *errorResp) Error() string {
163+ return fmt.Sprint("backend response:", err.Err.Message)
164+}
165+
166+const (
167+ hdrDATE = "Date"
168+ hdrSUBJECT = "Subject"
169+ hdrFROM = "From"
170+)
171
172=== added file 'plugins/dekko/dekko.go'
173--- plugins/dekko/dekko.go 1970-01-01 00:00:00 +0000
174+++ plugins/dekko/dekko.go 2016-07-22 11:44:01 +0000
175@@ -0,0 +1,346 @@
176+/*
177+ Copyright 2014 Canonical Ltd.
178+
179+ This program is free software: you can redistribute it and/or modify it
180+ under the terms of the GNU General Public License version 3, as published
181+ by the Free Software Foundation.
182+
183+ This program is distributed in the hope that it will be useful, but
184+ WITHOUT ANY WARRANTY; without even the implied warranties of
185+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
186+ PURPOSE. See the GNU General Public License for more details.
187+
188+ You should have received a copy of the GNU General Public License along
189+ with this program. If not, see <http://www.gnu.org/licenses/>.
190+*/
191+
192+package dekko
193+
194+import (
195+ "encoding/json"
196+ "fmt"
197+ "net/http"
198+ "net/mail"
199+ "net/url"
200+ "os"
201+ "regexp"
202+ "sort"
203+ "strings"
204+ "time"
205+
206+ "log"
207+
208+ "launchpad.net/account-polld/accounts"
209+ "launchpad.net/account-polld/gettext"
210+ "launchpad.net/account-polld/plugins"
211+ "launchpad.net/account-polld/qtcontact"
212+)
213+
214+const (
215+ APP_ID = "dekko.dekkoproject_dekko"
216+ dekkoDispatchUrl = "dekko://notify/%d/%s/%s"
217+ // If there's more than 10 emails in one batch, we don't show 10 notification
218+ // bubbles, but instead show one summary. We always show all notifications in the
219+ // indicator.
220+ individualNotificationsLimit = 10
221+ pluginName = "dekko"
222+)
223+
224+type reportedIdMap map[string]time.Time
225+
226+var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/")
227+
228+// timeDelta defines how old messages can be to be reported.
229+var timeDelta = time.Duration(time.Hour * 24)
230+
231+// trackDelta defines how old messages can be before removed from tracking
232+var trackDelta = time.Duration(time.Hour * 24 * 7)
233+
234+// relativeTimeDelta is the same as timeDelta
235+var relativeTimeDelta string = "1d"
236+
237+// regexp for identifying non-ascii characters
238+var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]")
239+
240+type GmailPlugin struct {
241+ // reportedIds holds the messages that have already been notified. This
242+ // approach is taken against timestamps as it avoids needing to call
243+ // get on the message.
244+ reportedIds reportedIdMap
245+ accountId uint
246+}
247+
248+func idsFromPersist(accountId uint) (ids reportedIdMap, err error) {
249+ err = plugins.FromPersist(pluginName, accountId, &ids)
250+ if err != nil {
251+ return nil, err
252+ }
253+ // discard old ids
254+ timestamp := time.Now()
255+ for k, v := range ids {
256+ delta := timestamp.Sub(v)
257+ if delta > trackDelta {
258+ log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta)
259+ delete(ids, k)
260+ }
261+ }
262+ return ids, nil
263+}
264+
265+func (ids reportedIdMap) persist(accountId uint) (err error) {
266+ err = plugins.Persist(pluginName, accountId, ids)
267+ if err != nil {
268+ log.Print("gmail plugin ", accountId, ": failed to save state: ", err)
269+ }
270+ return nil
271+}
272+
273+func New(accountId uint) *GmailPlugin {
274+ reportedIds, err := idsFromPersist(accountId)
275+ if err != nil {
276+ log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err)
277+ } else {
278+ log.Print("gmail plugin ", accountId, ": last state loaded from storage")
279+ }
280+ return &GmailPlugin{reportedIds: reportedIds, accountId: accountId}
281+}
282+
283+func (p *GmailPlugin) ApplicationId() plugins.ApplicationId {
284+ return plugins.ApplicationId(APP_ID)
285+}
286+
287+func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
288+ // This envvar check is to ease testing.
289+ if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" {
290+ authData.AccessToken = token
291+ }
292+
293+ resp, err := p.requestMessageList(authData.AccessToken)
294+ if err != nil {
295+ return nil, err
296+ }
297+ messages, err := p.parseMessageListResponse(resp)
298+ if err != nil {
299+ return nil, err
300+ }
301+
302+ // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch
303+ for i := range messages {
304+ resp, err := p.requestMessage(messages[i].Id, authData.AccessToken)
305+ if err != nil {
306+ return nil, err
307+ }
308+ messages[i], err = p.parseMessageResponse(resp)
309+ if err != nil {
310+ return nil, err
311+ }
312+ }
313+ notif, err := p.createNotifications(messages)
314+ if err != nil {
315+ return nil, err
316+ }
317+ return []*plugins.PushMessageBatch{
318+ &plugins.PushMessageBatch{
319+ Messages: notif,
320+ Limit: individualNotificationsLimit,
321+ OverflowHandler: p.handleOverflow,
322+ Tag: "dekko",
323+ }}, nil
324+
325+}
326+
327+func (p *GmailPlugin) reported(id string) bool {
328+ _, ok := p.reportedIds[id]
329+ return ok
330+}
331+
332+func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) {
333+ timestamp := time.Now()
334+ pushMsgMap := make(pushes)
335+
336+ for _, msg := range messages {
337+ hdr := msg.Payload.mapHeaders()
338+
339+ from := hdr[hdrFROM]
340+ var avatarPath string
341+
342+ emailAddress, err := mail.ParseAddress(from)
343+ if err != nil {
344+ // If the email address contains non-ascii characters, we get an
345+ // error so we're going to try again, this time mangling the name
346+ // by removing all non-ascii characters. We only care about the email
347+ // address here anyway.
348+ // XXX: We can't check the error message due to [1]: the error
349+ // message is different in go < 1.3 and > 1.5.
350+ // [1] https://github.com/golang/go/issues/12492
351+ mangledAddr := nonAsciiChars.ReplaceAllString(from, "")
352+ mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr)
353+ if mangledParseError == nil {
354+ emailAddress = mangledEmail
355+ }
356+ } else if emailAddress.Name != "" {
357+ // We only want the Name if the first ParseAddress
358+ // call was successful. I.e. we do not want the name
359+ // from a mangled email address.
360+ from = emailAddress.Name
361+ }
362+
363+ if emailAddress != nil {
364+ avatarPath = qtcontact.GetAvatar(emailAddress.Address)
365+ // If icon path starts with a path separator, assume local file path,
366+ // encode it and prepend file scheme defined in RFC 1738.
367+ if strings.HasPrefix(avatarPath, string(os.PathSeparator)) {
368+ avatarPath = url.QueryEscape(avatarPath)
369+ avatarPath = "file://" + avatarPath
370+ }
371+ }
372+
373+ msgStamp := hdr.getTimestamp()
374+
375+ if _, ok := pushMsgMap[msg.ThreadId]; ok {
376+ // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread
377+ pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from)
378+ } else if timestamp.Sub(msgStamp) < timeDelta {
379+ // TRANSLATORS: the %s is the "from" header corresponding to a specific email
380+ summary := fmt.Sprintf(gettext.Gettext("%s"), from)
381+ // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from"
382+ body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet)
383+ // fmt with label personal and threadId
384+ action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX", msg.Id)
385+ epoch := hdr.getEpoch()
386+ pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch)
387+ } else {
388+ log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta)
389+ }
390+ }
391+ pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap))
392+ for _, v := range pushMsgMap {
393+ pushMsg = append(pushMsg, v)
394+ }
395+ return pushMsg, nil
396+
397+}
398+func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage {
399+ // TODO it would probably be better to grab the estimate that google returns in the message list.
400+ approxUnreadMessages := len(pushMsg)
401+
402+ // TRANSLATORS: the %d refers to the number of new email messages.
403+ summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages)
404+
405+ body := ""
406+
407+ // fmt with label personal and no threadId
408+ action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX")
409+ epoch := time.Now().Unix()
410+
411+ return plugins.NewStandardPushMessage(summary, body, action, "", epoch)
412+}
413+
414+func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) {
415+ defer resp.Body.Close()
416+ decoder := json.NewDecoder(resp.Body)
417+
418+ if resp.StatusCode != http.StatusOK {
419+ var errResp errorResp
420+ if err := decoder.Decode(&errResp); err != nil {
421+ return nil, err
422+ }
423+ if errResp.Err.Code == 401 {
424+ return nil, plugins.ErrTokenExpired
425+ }
426+ return nil, &errResp
427+ }
428+
429+ var messages messageList
430+ if err := decoder.Decode(&messages); err != nil {
431+ return nil, err
432+ }
433+
434+ filteredMsg := p.messageListFilter(messages.Messages)
435+
436+ return filteredMsg, nil
437+}
438+
439+// messageListFilter returns a subset of unread messages where the subset
440+// depends on not being in reportedIds. Before returning, reportedIds is
441+// updated with the new list of unread messages.
442+func (p *GmailPlugin) messageListFilter(messages []message) []message {
443+ sort.Sort(byId(messages))
444+ var reportMsg []message
445+ var ids = make(reportedIdMap)
446+
447+ for _, msg := range messages {
448+ if !p.reported(msg.Id) {
449+ reportMsg = append(reportMsg, msg)
450+ }
451+ ids[msg.Id] = time.Now()
452+ }
453+ p.reportedIds = ids
454+ p.reportedIds.persist(p.accountId)
455+ return reportMsg
456+}
457+
458+func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) {
459+ defer resp.Body.Close()
460+ decoder := json.NewDecoder(resp.Body)
461+
462+ if resp.StatusCode != http.StatusOK {
463+ var errResp errorResp
464+ if err := decoder.Decode(&errResp); err != nil {
465+ return message{}, err
466+ }
467+ return message{}, &errResp
468+ }
469+
470+ var msg message
471+ if err := decoder.Decode(&msg); err != nil {
472+ return message{}, err
473+ }
474+
475+ return msg, nil
476+}
477+
478+func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) {
479+ u, err := baseUrl.Parse("messages/" + id)
480+ if err != nil {
481+ return nil, err
482+ }
483+
484+ query := u.Query()
485+ // only request specific fields
486+ query.Add("fields", "snippet,threadId,id,payload/headers")
487+ // get the full message to get From and Subject from headers
488+ query.Add("format", "full")
489+ u.RawQuery = query.Encode()
490+
491+ req, err := http.NewRequest("GET", u.String(), nil)
492+ if err != nil {
493+ return nil, err
494+ }
495+ req.Header.Set("Authorization", "Bearer "+accessToken)
496+
497+ return http.DefaultClient.Do(req)
498+}
499+
500+func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) {
501+ u, err := baseUrl.Parse("messages")
502+ if err != nil {
503+ return nil, err
504+ }
505+
506+ query := u.Query()
507+
508+ // get all unread inbox emails received after
509+ // the last time we checked. If this is the first
510+ // time we check, get unread emails after timeDelta
511+ query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta))
512+ u.RawQuery = query.Encode()
513+
514+ req, err := http.NewRequest("GET", u.String(), nil)
515+ if err != nil {
516+ return nil, err
517+ }
518+ req.Header.Set("Authorization", "Bearer "+accessToken)
519+
520+ return http.DefaultClient.Do(req)
521+}

Subscribers

People subscribed via source and target branches