Skip to content

Commit

Permalink
ざっくりメールのPush通知を受け取れる雰囲気にしてみた refs #1
Browse files Browse the repository at this point in the history
  • Loading branch information
sinmetal committed Dec 30, 2020
1 parent ca4774f commit 96d6b8d
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 16 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# ironhead

Gmail Webhock
23 changes: 22 additions & 1 deletion gmail_notify_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ type PubSubMessage struct {
Subscription string `json:"subscription"`
}

// NotifyData is Gmail Notify で飛んでくる中身
type NotifyData struct {
EmailAddress string `json:"emailAddress"`
HistoryID int64 `json:"historyId"`
}

// GmailNotifyPubSubHandler receives and processes a Pub/Sub push message.
func GmailNotifyPubSubHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var m PubSubMessage
body, err := ioutil.ReadAll(r.Body)
if err != nil {
Expand All @@ -32,5 +40,18 @@ func GmailNotifyPubSubHandler(w http.ResponseWriter, r *http.Request) {
return
}

fmt.Println(string(m.Message.Data))
var d NotifyData
if err := json.Unmarshal(m.Message.Data, &d); err != nil {
log.Printf("json.Unmarshal: %v", err)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}

info, err := gmailService.GetErrorReportingInfo(ctx, d.EmailAddress, d.HistoryID, tbfErrorReportingLabelID)
if err != nil {
log.Printf("gmailService.GetErrorReportingInfo: %v", err)
http.Error(w, "InternalServerError", http.StatusInternalServerError)
return
}
fmt.Printf("%+v", info)
}
56 changes: 56 additions & 0 deletions gmail_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ package main

import (
"context"
"encoding/base64"
"fmt"

"github.com/sinmetalcraft/ironhead/erroreportings"
"google.golang.org/api/gmail/v1"
)

var GmailServiceScope []string

func init() {
GmailServiceScope = append(GmailServiceScope, gmail.GmailReadonlyScope)
}

type GmailService struct {
gus *gmail.UsersService
}
Expand All @@ -23,6 +31,10 @@ type WatchResponse struct {
Expiration int64
}

// Watch is set watch request
// PubSub Topicに指定したLabelのメールが来たら、通知を行うWatchを設定する
// 1日1回呼ぶのがよいらしい
// https://developers.google.com/gmail/api/guides/push#watch_request
func (s *GmailService) Watch(ctx context.Context, userID string, req *gmail.WatchRequest) (*WatchResponse, error) {
res, err := s.gus.Watch(userID, req).Context(ctx).Do()
if err != nil {
Expand All @@ -33,3 +45,47 @@ func (s *GmailService) Watch(ctx context.Context, userID string, req *gmail.Watc
Expiration: res.Expiration,
}, nil
}

// GetMessage is historyIDから1件のメール・メッセージを取得する
func (s *GmailService) GetMessage(ctx context.Context, userID string, startHistoryID int64, labelID string) (*gmail.Message, error) {
res, err := s.gus.History.List(userID).StartHistoryId(uint64(startHistoryID)).LabelId(labelID).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("failed GmailUserService.Watch.userID=%s,req=%#v : %w", userID, err)
}

msg, err := s.gus.Messages.Get(userID, res.History[0].Messages[0].Id).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("failed GmailUserService.Message.Get.userID=%s,req=%#v : %w", userID, err)
}
return msg, nil
}

// GetErrorReportingInfo is startHistoryIDがErrorReportingのIDだと信じてGetする
func (s *GmailService) GetErrorReportingInfo(ctx context.Context, userID string, startHistoryID int64, labelID string) (*erroreportings.Info, error) {
msg, err := s.GetMessage(ctx, userID, startHistoryID, labelID)
if err != nil {
return nil, err
}

if len(msg.Payload.Parts) < 2 {
return nil, fmt.Errorf("invalid error reporting mail format")
}

var plainText []byte
var htmlText []byte
if msg.Payload.Parts[0].MimeType == "text/plain" {
plainText, err = base64.URLEncoding.DecodeString(msg.Payload.Parts[0].Body.Data)
if err != nil {
return nil, fmt.Errorf("invalid error reporting mail format")
}
}

if msg.Payload.Parts[1].MimeType == "text/html" {
htmlText, err = base64.URLEncoding.DecodeString(msg.Payload.Parts[1].Body.Data)
if err != nil {
return nil, fmt.Errorf("invalid error reporting mail format")
}
}

return erroreportings.Parse(string(plainText), string(htmlText)), nil
}
45 changes: 45 additions & 0 deletions gmail_service_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
package main

import (
"context"
"encoding/json"
"fmt"
"testing"

"cloud.google.com/go/pubsub"
"github.com/k0kubun/pp"
)

func TestPubSubPull(t *testing.T) {
ctx := context.Background()

pubsubClient, err := pubsub.NewClient(ctx, "sinmetal-ironhead")
if err != nil {
t.Fatal(err)
}

debugSub := pubsubClient.Subscription("gmail-debug")
err = debugSub.Receive(ctx, func(ctx context.Context, message *pubsub.Message) {
fmt.Printf("%+v\n", message.Attributes)
fmt.Printf("%s\n", string(message.Data))
var d NotifyData
if err := json.Unmarshal(message.Data, &d); err != nil {
t.Fatal(err)
}

})
if err != nil {
t.Fatal(err)
}
}

// 試しにErrorReportingのメッセージを取得してみる
func TestGmailService_GetMessage(t *testing.T) {
ctx := context.Background()

s := newGmailService(ctx)
msg, err := s.GetMessage(ctx, "[email protected]", 12158234, "Label_6698128523804588152")
if err != nil {
t.Fatal(err)
}
pp.Print(msg)
}
25 changes: 25 additions & 0 deletions gmail_watch_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"
"net/http"

"google.golang.org/api/gmail/v1"
)

// GmailWatchHandler is gmail.Watch を実行する
// 1日に1回cronで実行する
func GmailWatchHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

resp, err := gmailService.Watch(ctx, userID, &gmail.WatchRequest{
TopicName: "projects/sinmetal-ironhead/topics/gmail",
LabelIds: []string{tbfErrorReportingLabelID},
})
if err != nil {
fmt.Printf("failed GmailService.Watch. err=%+v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Printf("HistoryID:%d,Expiration:%d\n", resp.HistoryID, resp.Expiration)
}
80 changes: 80 additions & 0 deletions golden_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"context"
"encoding/json"
"io/ioutil"
"testing"

"cloud.google.com/go/storage"
)

const goldenBucket = "sinmetal-ironhead-golden"

func TestCreateGolden(t *testing.T) {
ctx := context.Background()

s := newGmailService(t)

const userID = "[email protected]"
const startHistoryID = 12158234
const labelID = "Label_6698128523804588152"

res, err := s.gus.History.List(userID).StartHistoryId(uint64(startHistoryID)).LabelId(labelID).Context(ctx).Do()
if err != nil {
t.Fatal(err)
}
//pp.Print(res)
msg, err := s.gus.Messages.Get(userID, res.History[0].Messages[0].Id).Context(ctx).Do()
if err != nil {
t.Fatal(err)
}
writeGoldenFile(ctx, "error-reporting-mail-message.json", msg, t)
}

func writeGoldenFile(ctx context.Context, path string, body interface{}, t *testing.T) {
gcs, err := storage.NewClient(ctx)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := gcs.Close(); err != nil {
t.Fatal(err)
}
}()

w := gcs.Bucket(goldenBucket).Object(path).NewWriter(ctx)
if err := json.NewEncoder(w).Encode(body); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
}

func readGoldenFile(ctx context.Context, path string, t *testing.T) []byte {
gcs, err := storage.NewClient(ctx)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := gcs.Close(); err != nil {
t.Fatal(err)
}
}()

r, err := gcs.Bucket(goldenBucket).Object(path).NewReader(ctx)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := r.Close(); err != nil {
t.Fatal(err)
}
}()
b, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
return b
}
30 changes: 15 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import (
"google.golang.org/api/option"
)

const (
userID = "[email protected]"
tbfErrorReportingLabelID = "Label_6698128523804588152"
)

var gmailService *GmailService

func main() {
var (
cmd = flag.String("cmd", "default", "command")
Expand All @@ -26,12 +33,15 @@ func main() {
os.Exit(0)
}

watchGmail()
ctx := context.Background()

gmailService = newGmailService(ctx)

const addr = ":8080"
fmt.Printf("Start Listen %s\n", addr)

http.HandleFunc("/notify/gmail", GmailNotifyPubSubHandler)
http.HandleFunc("/gmail/watch", GmailWatchHandler)
http.HandleFunc("/", helloWorldHandler)
log.Fatal(http.ListenAndServe(addr, nil))
}
Expand All @@ -58,14 +68,12 @@ func saveToken() {
}
}()

if err := ts.SaveToken(ctx, gmail.GmailMetadataScope); err != nil {
if err := ts.SaveToken(ctx, GmailServiceScope...); err != nil {
panic(err)
}
}

func watchGmail() {
ctx := context.Background()

func newGmailService(ctx context.Context) *GmailService {
gcs, err := storage.NewClient(ctx)
if err != nil {
log.Fatalf("failed storage.NewClient err=%#v", err)
Expand All @@ -80,7 +88,7 @@ func watchGmail() {
}
}()

client, err := ts.CreateHTTPClient(ctx, gmail.GmailMetadataScope)
client, err := ts.CreateHTTPClient(ctx, GmailServiceScope...)
if err != nil {
panic(err)
}
Expand All @@ -94,13 +102,5 @@ func watchGmail() {
if err != nil {
log.Fatalf("failed NewGmailService err=%+v", err)
}

resp, err := s.Watch(ctx, "[email protected]", &gmail.WatchRequest{
TopicName: "projects/sinmetal-ironhead/topics/gmail",
LabelIds: []string{"Label_6698128523804588152"},
})
if err != nil {
log.Fatalf("failed Watch err=%+v", err)
}
fmt.Printf("%#v", resp)
return s
}

0 comments on commit 96d6b8d

Please sign in to comment.