From 088728914941cf6ab405c2d19ae5138a5dbf41c7 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 7 Oct 2021 01:51:13 +0200 Subject: [PATCH] #522: Fixed hybrid gmail and outlook app to properly work --- backend/go-app/go.mod | 2 +- backend/go-app/main.go | 48 +- backend/go-app/oauth2.go | 1666 ------------------------ backend/go-app/walkoff.go | 9 +- frontend/src/views/AngularWorkflow.jsx | 102 +- frontend/src/views/AppCreator.jsx | 2 + frontend/src/views/Apps.jsx | 7 +- frontend/src/views/Docs.jsx | 14 +- 8 files changed, 119 insertions(+), 1731 deletions(-) delete mode 100644 backend/go-app/oauth2.go diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index c152f70e..749f1507 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,7 +2,7 @@ module shuffle go 1.13 -//replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared +replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 2a8c1f6f..ea6c9d16 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -7,7 +7,7 @@ import ( "bytes" "context" "crypto/md5" - "crypto/tls" + //"crypto/tls" //"crypto/x509" "encoding/hex" "encoding/json" @@ -49,7 +49,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/storage/memory" - githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + //githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" // Random xj "github.com/basgys/goxml2json" @@ -59,7 +59,7 @@ import ( "gopkg.in/yaml.v3" // PROXY overrides - "gopkg.in/src-d/go-git.v4/plumbing/transport/client" + //"gopkg.in/src-d/go-git.v4/plumbing/transport/client" // githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" // Web @@ -3591,7 +3591,7 @@ func handleCloudJob(job shuffle.CloudSyncJob) error { if job.Action == "execute" { // FIXME: Get the email ctx := context.Background() - maildata := shuffle.MailData{} + maildata := shuffle.MailDataOutlook{} err := json.Unmarshal([]byte(job.ThirdItem), &maildata) if err != nil { log.Printf("Maildata unmarshal error: %s", err) @@ -3613,7 +3613,7 @@ func handleCloudJob(job shuffle.CloudSyncJob) error { return err } - emails, err := getOutlookEmail(outlookClient, maildata) + emails, err := shuffle.GetOutlookEmail(outlookClient, maildata) //log.Printf("EMAILS: %d", len(emails)) //log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) @@ -4135,14 +4135,14 @@ func runInitEs(ctx context.Context) { log.Printf("[INFO] Getting and validating workflowapps. Got %d with err %#v", len(workflowapps), err) // accept any certificate (might be useful for testing) - customGitClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - Timeout: 15 * time.Second, - } - client.InstallProtocol("http", githttp.NewClient(customGitClient)) - client.InstallProtocol("https", githttp.NewClient(customGitClient)) + //customGitClient := &http.Client{ + // Transport: &http.Transport{ + // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + // }, + // Timeout: 15 * time.Second, + //} + //client.InstallProtocol("http", githttp.NewClient(customGitClient)) + //client.InstallProtocol("https", githttp.NewClient(customGitClient)) if err != nil && len(workflowapps) == 0 { log.Printf("[WARNING] Failed getting apps (runInit): %s", err) @@ -5855,20 +5855,20 @@ func initHandlers() { r.HandleFunc("/api/v1/get_openapi/{key}", getOpenapi).Methods("GET", "OPTIONS") // Specific triggers - r.HandleFunc("/api/v1/workflows/{key}/outlook", handleCreateOutlookSub).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", handleDeleteOutlookSub).Methods("DELETE", "OPTIONS") - r.HandleFunc("/api/v1/triggers/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/outlook", shuffle.HandleCreateOutlookSub).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", shuffle.HandleDeleteOutlookSub).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/triggers/outlook/register", shuffle.HandleNewOutlookRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/outlook/getFolders", shuffle.HandleGetOutlookFolders).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/triggers/outlook/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/triggers/gmail/register", handleNewGmailRegister).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/triggers/outlook/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS") // FIXME: This should only be cloud. Done locally with ngrok to test - r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS") + //r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/triggers/gmail/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}/gmail", handleCreateGmailSub).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}/gmail/{triggerId}", handleDeleteGmailSub).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/triggers/gmail/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/gmail", shuffle.HandleCreateGmailSub).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/gmail/{triggerId}", shuffle.HandleDeleteGmailSub).Methods("DELETE", "OPTIONS") //r.HandleFunc("/api/v1/triggers/gmail/{key}", handleGetSpecificGmailTrigger).Methods("GET", "OPTIONS") //r.HandleFunc("/api/v1/triggers/outlook/getFolders", shuffle.HandleGetOutlookFolders).Methods("GET", "OPTIONS") @@ -5879,8 +5879,8 @@ func initHandlers() { // EVERYTHING below here is NEW for 0.8.0 (written 25.05.2021) r.HandleFunc("/api/v1/workflows/{key}/publish", makeWorkflowPublic).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/orgs", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/orgs/", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/orgs", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/orgs/", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleGetOrg).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleEditOrg).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/create_sub_org", shuffle.HandleCreateSubOrg).Methods("POST", "OPTIONS") diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go deleted file mode 100644 index d2f0b2f5..00000000 --- a/backend/go-app/oauth2.go +++ /dev/null @@ -1,1666 +0,0 @@ -package main - -import ( - "github.com/frikky/shuffle-shared" - - "bytes" - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "strings" - "time" - //"golang.org/x/oauth2" -) - -func getOutlookAttachment(client *http.Client, emailId, attachmentId string) ([]shuffle.FullEmail, error) { - //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") - - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s/attachments/%s", emailId, attachmentId) - //log.Printf("Outlook email URL: %#v", requestUrl) - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("[INFO] OutlookErr: %s", err) - return []shuffle.FullEmail{}, err - } - - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("[WARNING] Failed body decoding from outlook email") - return []shuffle.FullEmail{}, err - } - - //type FullEmail struct { - log.Printf("[INFO] Attachment Body: %s", string(body)) - log.Printf("[INFO] Status email: %d", ret.StatusCode) - if ret.StatusCode != 200 { - return []shuffle.FullEmail{}, err - } - - //log.Printf("Body: %s", string(body)) - - /* - parsedmail := FullEmail{} - err = json.Unmarshal(body, &parsedmail) - if err != nil { - log.Printf("[INFO] Email unmarshal error: %s", err) - return []FullEmail{}, err - } - - emails = append(emails, parsedmail) - */ - - return []shuffle.FullEmail{}, nil -} - -func getOutlookEmail(client *http.Client, maildata shuffle.MailData) ([]shuffle.FullEmail, error) { - //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") - - emails := []shuffle.FullEmail{} - for _, email := range maildata.Value { - //messageId := email.Resourcedata.ID - //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/%s", messageId) - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s", email.Resource) - //log.Printf("Outlook email URL: %#v", requestUrl) - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("[INFO] OutlookErr: %s", err) - return []shuffle.FullEmail{}, err - } - - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("[WARNING] Failed body decoding from outlook email") - return []shuffle.FullEmail{}, err - } - - //type shuffle.FullEmail struct { - //log.Printf("[INFO] EMAIL Body: %s", string(body)) - //log.Printf("[INFO] Status email: %d", ret.StatusCode) - if ret.StatusCode != 200 { - return []shuffle.FullEmail{}, err - } - - //log.Printf("Body: %s", string(body)) - - parsedmail := shuffle.FullEmail{} - err = json.Unmarshal(body, &parsedmail) - if err != nil { - log.Printf("[INFO] Email unmarshal error: %s", err) - return []shuffle.FullEmail{}, err - } - - emails = append(emails, parsedmail) - } - - return emails, nil -} - -func getOutlookProfile(client *http.Client) (shuffle.OutlookProfile, error) { - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail") - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("[INFO] Folder error: %s", err) - return shuffle.OutlookProfile{}, err - } - - log.Printf("[INFO] Status profile: %d", ret.StatusCode) - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("[INFO] Body: %s", err) - return shuffle.OutlookProfile{}, err - } - - log.Printf("[INFO] BODY: %s", string(body)) - - profile := shuffle.OutlookProfile{} - err = json.Unmarshal(body, &profile) - if err != nil { - log.Printf("Unmarshal: %s", err) - return shuffle.OutlookProfile{}, err - } - - return profile, nil -} - -func handleNewGmailRegister(resp http.ResponseWriter, request *http.Request) { - cors := shuffle.HandleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("[INFO] Api authentication failed in getting specific trigger: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - code := request.URL.Query().Get("code") - if len(code) == 0 { - log.Println("No code") - resp.WriteHeader(401) - return - } - - url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) - log.Printf("URL: %s", url) - ctx := context.Background() - _, accessToken, err := shuffle.GetGmailClient(ctx, code, shuffle.OauthToken{}, url) - if err != nil { - log.Printf("Oauth client failure - outlook register: %s", err) - resp.WriteHeader(401) - return - } - - // This should be possible, and will also give the actual username - - /* - profile, err := getOutlookProfile(client) - if err != nil { - log.Printf("Outlook profile failure: %s", err) - resp.WriteHeader(401) - return - } - */ - - // This is a state workaround, which should really be for CSRF checks lol - state := request.URL.Query().Get("state") - if len(state) == 0 { - log.Println("No state") - resp.WriteHeader(401) - return - } - - stateitems := strings.Split(state, "%26") - if len(stateitems) == 1 { - stateitems = strings.Split(state, "&") - } - - // FIXME - trigger auth - senderUser := "" - trigger := shuffle.TriggerAuth{} - for _, item := range stateitems { - itemsplit := strings.Split(item, "%3D") - if len(itemsplit) == 1 { - itemsplit = strings.Split(item, "=") - } - - if len(itemsplit) != 2 { - continue - } - - //log.Printf("ITEM: %#v", itemsplit) - - // Do something here - if itemsplit[0] == "workflow_id" { - trigger.WorkflowId = itemsplit[1] - } else if itemsplit[0] == "trigger_id" { - trigger.Id = itemsplit[1] - } else if itemsplit[0] == "type" { - trigger.Type = itemsplit[1] - } else if itemsplit[0] == "start" { - trigger.Start = itemsplit[1] - } else if itemsplit[0] == "username" { - trigger.Username = itemsplit[1] - trigger.Owner = itemsplit[1] - senderUser = itemsplit[1] - } - } - - // THis is an override based on the user in oauth return - /* - if len(profile.Mail) > 0 { - trigger.Username = profile.Mail - } - */ - - trigger.Code = code - trigger.OauthToken = shuffle.OauthToken{ - AccessToken: accessToken.AccessToken, - TokenType: accessToken.TokenType, - RefreshToken: accessToken.RefreshToken, - Expiry: accessToken.Expiry, - } - - //log.Printf("Done with client: %#v", accessToken) - //log.Printf("Done with client2: %#v", trigger.OauthToken) - //resp.WriteHeader(401) - //return - - //log.Printf("%#v", trigger) - log.Println(trigger.WorkflowId) - log.Println(trigger.Id) - log.Println(senderUser) - log.Println(trigger.Type) - log.Printf("STARTNODE: %s", trigger.Start) - log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) - if trigger.WorkflowId == "" || trigger.Id == "" || senderUser == "" || trigger.Type == "" { - log.Printf("[INFO] All oauth items need to contain data to register a new state") - resp.WriteHeader(401) - return - } - - // Should also update the user - Userdata, err := shuffle.GetUser(ctx, user.Id) - if err != nil { - log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) - resp.WriteHeader(401) - return - } - - Userdata.Authentication = append(Userdata.Authentication, shuffle.UserAuth{ - Name: "Gmail", - Description: "oauth2", - Workflows: []string{trigger.WorkflowId}, - Username: trigger.Username, - Fields: []shuffle.UserAuthField{ - shuffle.UserAuthField{ - Key: "trigger_id", - Value: trigger.Id, - }, - shuffle.UserAuthField{ - Key: "username", - Value: trigger.Username, - }, - shuffle.UserAuthField{ - Key: "code", - Value: code, - }, - shuffle.UserAuthField{ - Key: "type", - Value: trigger.Type, - }, - }, - }) - - // Set apikey for the user if they don't have one - err = shuffle.SetUser(ctx, Userdata, false) - if err != nil { - log.Printf("[WARNING] Failed setting user data for %s: %s (gmail)", Userdata.Username, err) - resp.WriteHeader(401) - return - } - - err = shuffle.SetTriggerAuth(ctx, trigger) - if err != nil { - log.Printf("[WARNING] Failed to set trigger auth for %s - %s (gmail)", trigger.Username, err) - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { - cors := shuffle.HandleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("[INFO] Api authentication failed in getting specific trigger: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - code := request.URL.Query().Get("code") - if len(code) == 0 { - log.Println("No code") - resp.WriteHeader(401) - return - } - - url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) - log.Println(url) - ctx := context.Background() - _, accessToken, err := shuffle.GetOutlookClient(ctx, code, shuffle.OauthToken{}, url) - if err != nil { - log.Printf("Oauth client failure - outlook register: %s", err) - resp.WriteHeader(401) - return - } - - // This should be possible, and will also give the actual username - - /* - profile, err := getOutlookProfile(client) - if err != nil { - log.Printf("Outlook profile failure: %s", err) - resp.WriteHeader(401) - return - } - */ - - // This is a state workaround, which should really be for CSRF checks lol - state := request.URL.Query().Get("state") - if len(state) == 0 { - log.Println("No state") - resp.WriteHeader(401) - return - } - - stateitems := strings.Split(state, "%26") - if len(stateitems) == 1 { - stateitems = strings.Split(state, "&") - } - - // FIXME - trigger auth - senderUser := "" - trigger := shuffle.TriggerAuth{} - for _, item := range stateitems { - itemsplit := strings.Split(item, "%3D") - if len(itemsplit) == 1 { - itemsplit = strings.Split(item, "=") - } - - if len(itemsplit) != 2 { - continue - } - - //log.Printf("ITEM: %#v", itemsplit) - - // Do something here - if itemsplit[0] == "workflow_id" { - trigger.WorkflowId = itemsplit[1] - } else if itemsplit[0] == "trigger_id" { - trigger.Id = itemsplit[1] - } else if itemsplit[0] == "type" { - trigger.Type = itemsplit[1] - } else if itemsplit[0] == "start" { - trigger.Start = itemsplit[1] - } else if itemsplit[0] == "username" { - trigger.Username = itemsplit[1] - trigger.Owner = itemsplit[1] - senderUser = itemsplit[1] - } - } - - // THis is an override based on the user in oauth return - /* - if len(profile.Mail) > 0 { - trigger.Username = profile.Mail - } - */ - - trigger.Code = code - trigger.OauthToken = shuffle.OauthToken{ - AccessToken: accessToken.AccessToken, - TokenType: accessToken.TokenType, - RefreshToken: accessToken.RefreshToken, - Expiry: accessToken.Expiry, - } - - //log.Printf("%#v", trigger) - log.Println(trigger.WorkflowId) - log.Println(trigger.Id) - log.Println(senderUser) - log.Println(trigger.Type) - log.Printf("STARTNODE: %s", trigger.Start) - log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) - if trigger.WorkflowId == "" || trigger.Id == "" || senderUser == "" || trigger.Type == "" { - log.Printf("[INFO] All oauth items need to contain data to register a new state") - resp.WriteHeader(401) - return - } - - // Should also update the user - Userdata, err := shuffle.GetUser(ctx, user.Id) - if err != nil { - log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) - resp.WriteHeader(401) - return - } - - Userdata.Authentication = append(Userdata.Authentication, shuffle.UserAuth{ - Name: "Outlook", - Description: "oauth2", - Workflows: []string{trigger.WorkflowId}, - Username: trigger.Username, - Fields: []shuffle.UserAuthField{ - shuffle.UserAuthField{ - Key: "trigger_id", - Value: trigger.Id, - }, - shuffle.UserAuthField{ - Key: "username", - Value: trigger.Username, - }, - shuffle.UserAuthField{ - Key: "code", - Value: code, - }, - shuffle.UserAuthField{ - Key: "type", - Value: trigger.Type, - }, - }, - }) - - // Set apikey for the user if they don't have one - err = shuffle.SetUser(ctx, Userdata, false) - if err != nil { - log.Printf("[WARNING] Failed setting user data for %s: %s (outlook)", Userdata.Username, err) - resp.WriteHeader(401) - return - } - - err = shuffle.SetTriggerAuth(ctx, trigger) - if err != nil { - log.Printf("[WARNING] Failed to set trigger auth for %s - %s (outlook)", trigger.Username, err) - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in getting specific workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[5] - } - - if strings.Contains(workflowId, "?") { - workflowId = strings.Split(workflowId, "?")[0] - } - - ctx := context.Background() - trigger, err := shuffle.GetTriggerAuth(ctx, workflowId) - if err != nil { - log.Printf("[INFO] Trigger %s doesn't exist - specific trigger.", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - if user.Username != trigger.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for trigger %s", user.Username, trigger.Id) - resp.WriteHeader(401) - return - } - - trigger.OauthToken = shuffle.OauthToken{} - trigger.Code = "" - - b, err := json.Marshal(trigger) - if err != nil { - log.Println("Failed to marshal data") - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write(b) -} - -// Lists the users current subscriptions -func getOutlookSubscriptions(outlookClient *http.Client) (SubscriptionsWrapper, error) { - fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions") - req, err := http.NewRequest( - "GET", - fullUrl, - nil, - ) - req.Header.Add("Content-Type", "application/json") - res, err := outlookClient.Do(req) - if err != nil { - log.Printf("suberror Client: %s", err) - return SubscriptionsWrapper{}, err - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Suberror Body: %s", err) - return SubscriptionsWrapper{}, err - } - - newSubs := SubscriptionsWrapper{} - err = json.Unmarshal(body, &newSubs) - if err != nil { - return SubscriptionsWrapper{}, err - } - - return newSubs, nil -} - -type SubscriptionsWrapper struct { - OdataContext string `json:"@odata.context"` - Value []OutlookSubscription `json:"value"` -} - -type OutlookSubscription struct { - ChangeType string `json:"changeType"` - NotificationURL string `json:"notificationUrl"` - Resource string `json:"resource"` - ExpirationDateTime string `json:"expirationDateTime"` - ClientState string `json:"clientState"` - Id string `json:"id"` -} - -type GmailSubscription struct { - TopicName string `json:"topicName"` - LabelIds []string `json:"labelIds"` -} - -func makeGmailSubscription(client *http.Client, folderIds []string, notificationURL string) (shuffle.SubResponse, error) { - fullUrl := "https://www.googleapis.com/gmail/v1/users/me/watch" - - // FIXME - this expires rofl - //t := time.Now().Local().Add(time.Minute * time.Duration(4200)) - //timeFormat := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.0000000Z", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) - - resource := "projects/shuffler/topics/gmail_testing" - log.Printf("[INFO] Subscription resource to get(s) for gmail: %s", resource) - sub := GmailSubscription{ - TopicName: resource, - LabelIds: folderIds, - } - //ClientState: "This is a test", - - data, err := json.Marshal(sub) - if err != nil { - log.Printf("Marshal: %s", err) - return shuffle.SubResponse{}, err - } - - req, err := http.NewRequest( - "POST", - fullUrl, - bytes.NewBuffer(data), - ) - req.Header.Add("Content-Type", "application/json") - res, err := client.Do(req) - if err != nil { - log.Printf("[WARNING] GMAIL Client: %s", err) - return shuffle.SubResponse{}, err - } - - log.Printf("[INFO] Subscription on GMAIL Status: %d", res.StatusCode) - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("[WARNING] Gmail subscription Body: %s", err) - return shuffle.SubResponse{}, err - } - - log.Printf("GMAIL RESP: %s", string(body)) - - if res.StatusCode != 200 && res.StatusCode != 201 { - return shuffle.SubResponse{}, errors.New(fmt.Sprintf("Subscription failed: %s", string(body))) - } - - // Use data from body here to create thingy - newSub := shuffle.SubResponse{} - err = json.Unmarshal(body, &newSub) - if err != nil { - return shuffle.SubResponse{}, err - } - - return newSub, nil -} - -func makeOutlookSubscription(client *http.Client, folderIds []string, notificationURL string) (string, error) { - fullUrl := "https://graph.microsoft.com/v1.0/subscriptions" - - // FIXME - this expires rofl - t := time.Now().Local().Add(time.Minute * time.Duration(4200)) - timeFormat := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.0000000Z", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) - - resource := fmt.Sprintf("me/mailfolders('%s')/messages", strings.Join(folderIds, "','")) - log.Printf("[INFO] Subscription resource to get(s): %s", resource) - sub := OutlookSubscription{ - ChangeType: "created", - ClientState: "Shuffle subscription", - NotificationURL: notificationURL, - ExpirationDateTime: timeFormat, - Resource: resource, - } - //ClientState: "This is a test", - - data, err := json.Marshal(sub) - if err != nil { - log.Printf("Marshal: %s", err) - return "", err - } - - req, err := http.NewRequest( - "POST", - fullUrl, - bytes.NewBuffer(data), - ) - req.Header.Add("Content-Type", "application/json") - - res, err := client.Do(req) - if err != nil { - log.Printf("Client: %s", err) - return "", err - } - - log.Printf("[INFO] Subscription Status: %d", res.StatusCode) - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Body: %s", err) - return "", err - } - - if res.StatusCode != 200 && res.StatusCode != 201 { - return "", errors.New(fmt.Sprintf("Subscription failed: %s", string(body))) - } - - // Use data from body here to create thingy - newSub := OutlookSubscription{} - err = json.Unmarshal(body, &newSub) - if err != nil { - return "", err - } - - return newSub.Id, nil -} - -// Basically the same as a webhook -func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) { - path := strings.Split(request.URL.String(), "/") - if len(path) < 4 { - log.Printf("[INFO] Bad outlook callback URL: %s", path) - resp.WriteHeader(403) - resp.Write([]byte(`{"success": false}`)) - return - } - - // 1. Get config with hookId - //fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId) - ctx := context.Background() - location := strings.Split(request.URL.String(), "/") - - var hookId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - hookId = location[5] - } - - // ID: webhook_ - if len(hookId) != 36 { - log.Printf("[WARNING] Bad hook ID: %s (%d)", hookId, len(hookId)) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "message": "ID not valid"}`)) - return - } - - //func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { - hook, err := shuffle.GetTriggerAuth(ctx, hookId) - if err != nil { - log.Printf("[INFO] Failed getting trigger %s (callback): %s", hookId, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("[INFO] Body data error: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - //log.Printf("[INFO] BODY: %s. Len: %d", string(body), len(body)) - //key, ok := request.URL.Query()["validationToken"] - //if ok { - //} - token := request.URL.Query().Get("validationToken") - if len(body) == 0 && len(token) > 0 { - log.Printf("[INFO] Should handle trigger token %s", token) - resp.WriteHeader(200) - resp.Write([]byte(string(token))) - return - } - - // 1. Take the body and parse data -> Get the email itself - - maildata := shuffle.MailData{} - err = json.Unmarshal(body, &maildata) - if err != nil { - log.Printf("Maildata unmarshal error: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - redirectDomain := "localhost:5001" - redirectUrl := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) - outlookClient, _, err := shuffle.GetOutlookClient(ctx, "", hook.OauthToken, redirectUrl) - if err != nil { - log.Printf("Oauth client failure - triggerauth: %s", err) - resp.WriteHeader(401) - return - } - - emails, err := getOutlookEmail(outlookClient, maildata) - log.Printf("[INFO] EMAILS: %d. If this is more than 1, please contact frikky@shuffler.io", len(emails)) - //log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) - - //type shuffle.FullEmail struct { - email := shuffle.FullEmail{} - if len(emails) == 1 { - email = emails[0] - } - - // Parse indicators (domains, emails, ips, domains etc)! - newEmail := shuffle.ParsedShuffleMail{} - newEmail.Body.ContentType = email.Body.Contenttype - newEmail.Body.Content = email.Body.Content - newEmail.Body.RawBody = email.Body.Content - - newEmail.Header.Subject = email.Subject - newEmail.Header.From = email.From.Emailaddress.Address - for _, to := range email.Torecipients { - newEmail.Header.To = append(newEmail.Header.To, to.Emailaddress.Address) - } - newEmail.Header.Date = email.Receiveddatetime.String() - - newEmail.MessageID = email.ID - - if email.Hasattachments { - log.Printf("SHOULD HANDLE ATTACHMENTS FOR EMAIL!") - - for _, attachment := range email.Attachments { - parsedAttachment, err := getOutlookAttachment(outlookClient, email.ID, attachment.ID) - if err != nil { - log.Printf("Failed attachment %s: %s", attachment.ID, err) - continue - } - - log.Printf("ATTACHMENT: %#v", parsedAttachment) - } - //log.Printf("%#v", attachments) - //log.Printf("%s", err) - //GET /users/{id | userPrincipalName}/events/{id}/attachments/{id} - } - - emailBytes, err := json.Marshal(email) - if err != nil { - log.Printf("[INFO] Failed email marshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - type ExecutionStruct struct { - Start string `json:"start"` - ExecutionSource string `json:"execution_source"` - ExecutionArgument string `json:"execution_argument"` - } - - newBody := ExecutionStruct{ - Start: hook.Start, - ExecutionSource: "outlook", - ExecutionArgument: string(emailBytes), - } - - b, err := json.Marshal(newBody) - if err != nil { - log.Printf("[INFO] Failed newBody marshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - baseUrl := &url.URL{} - newRequest := &http.Request{ - URL: baseUrl, - Method: "POST", - Body: ioutil.NopCloser(bytes.NewReader(b)), - } - - workflow := shuffle.Workflow{ - ID: "", - } - - // OrgId: activeOrgs[0].Id, - workflowExecution, executionResp, err := handleExecution(hook.WorkflowId, workflow, newRequest) - if err == nil { - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) - return - } - - resp.WriteHeader(500) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) -} - -func removeOutlookSubscription(outlookClient *http.Client, subscriptionId string) error { - // DELETE https://graph.microsoft.com/v1.0/subscriptions/{id} - fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions/%s", subscriptionId) - req, err := http.NewRequest( - "DELETE", - fullUrl, - nil, - ) - req.Header.Add("Content-Type", "application/json") - res, err := outlookClient.Do(req) - if err != nil { - log.Printf("Client: %s", err) - return err - } - - if res.StatusCode != 200 && res.StatusCode != 201 && res.StatusCode != 204 { - return errors.New(fmt.Sprintf("Bad status code when deleting subscription: %d", res.StatusCode)) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Body: %s", err) - return err - } - - _ = body - - return nil -} - -func handleGmailSubRemoval(ctx context.Context, user shuffle.User, workflowId, triggerId string) error { - // 1. Get the auth for trigger - // 2. Stop the subscription - // 3. Remove the function - // 4. Remove the database entry for auth - trigger, err := shuffle.GetTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) - return err - } - //if runningEnvironment != "cloud" { - // log.Printf("[INFO] SHOULD STOP OUTLOOK SUB ONPREM SYNC WITH CLOUD for workflow ID %s", workflowId) - // org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) - // if err != nil { - // log.Printf("[INFO] Failed finding org %s during outlook removal: %s", org.Id, err) - // return err - // } - - // log.Printf("[INFO] Stopping cloud configuration for trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId) - // action := shuffle.CloudSyncJob{ - // Type: "outlook", - // Action: "stop", - // OrgId: org.Id, - // PrimaryItemId: trigger.Id, - // SecondaryItem: trigger.Start, - // ThirdItem: trigger.WorkflowId, - // } - - // err = executeCloudAction(action, org.SyncConfig.Apikey) - // if err != nil { - // log.Printf("[INFO] Failed cloud action STOP outlook execution: %s", err) - // return err - // } else { - // log.Printf("[INFO] Successfully set STOPPED outlook execution trigger") - // } - //} else { - // log.Printf("SHOULD STOP OUTLOOK SUB IN CLOUD") - //} - - // Actually delete the thing - redirectDomain := "localhost:5001" - url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) - gmailClient, _, err := shuffle.GetGmailClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("[WARNING] Oauth client failure - gmail delete: %s", err) - return err - } - - // bytes.NewBuffer(data) - fullUrl := "https://www.googleapis.com/gmail/v1/users/me/stop" - req, err := http.NewRequest( - "POST", - fullUrl, - nil, - ) - req.Header.Add("Content-Type", "application/json") - res, err := gmailClient.Do(req) - if err != nil { - log.Printf("[WARNING] GMAIL Client (2): %s", err) - return err - } - - log.Printf("[INFO] Stop subscription on GMAIL Status: %d", res.StatusCode) - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("[WARNING] Gmail subscription Body (2): %s", err) - return err - } - - log.Printf("GMAIL RESP (%d): %s", res.StatusCode, string(body)) - - if res.StatusCode != 200 && res.StatusCode != 201 { - return errors.New(fmt.Sprintf("Subscription failed: %s", string(body))) - } - //notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) - //curSubscriptions, err := getOutlookSubscriptions(outlookClient) - //if err == nil { - // for _, sub := range curSubscriptions.Value { - // if sub.NotificationURL == notificationURL { - // log.Printf("[INFO] Removing subscription %s from gmail for %s", sub.Id, workflowId) - // removeOutlookSubscription(outlookClient, sub.Id) - // } - // } - //} else { - // log.Printf("Failed to get subscriptions - need to overwrite") - //} - - return nil -} - -// Remove AUTH -// Remove function -// Remove subscription -func handleOutlookSubRemoval(ctx context.Context, user shuffle.User, workflowId, triggerId string) error { - // 1. Get the auth for trigger - // 2. Stop the subscription - // 3. Remove the function - // 4. Remove the database entry for auth - trigger, err := shuffle.GetTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) - return err - } - - if runningEnvironment != "cloud" { - log.Printf("[INFO] SHOULD STOP OUTLOOK SUB ONPREM SYNC WITH CLOUD for workflow ID %s", workflowId) - org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("[INFO] Failed finding org %s during outlook removal: %s", org.Id, err) - return err - } - - log.Printf("[INFO] Stopping cloud configuration for trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId) - action := shuffle.CloudSyncJob{ - Type: "outlook", - Action: "stop", - OrgId: org.Id, - PrimaryItemId: trigger.Id, - SecondaryItem: trigger.Start, - ThirdItem: trigger.WorkflowId, - } - - err = executeCloudAction(action, org.SyncConfig.Apikey) - if err != nil { - log.Printf("[INFO] Failed cloud action STOP outlook execution: %s", err) - return err - } else { - log.Printf("[INFO] Successfully set STOPPED outlook execution trigger") - } - } else { - log.Printf("SHOULD STOP OUTLOOK SUB IN CLOUD") - } - - // Actually delete the thing - redirectDomain := "localhost:5001" - url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) - outlookClient, _, err := shuffle.GetOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("[WARNING] Oauth client failure - outlook folders: %s", err) - return err - } - notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) - curSubscriptions, err := getOutlookSubscriptions(outlookClient) - if err == nil { - for _, sub := range curSubscriptions.Value { - if sub.NotificationURL == notificationURL { - log.Printf("[INFO] Removing subscription %s from o365 for workflow %s", sub.Id, workflowId) - removeOutlookSubscription(outlookClient, sub.Id) - } - } - } else { - log.Printf("Failed to get subscriptions - need to overwrite") - } - - return nil -} - -func handleDeleteGmailSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - var triggerId string - if location[1] == "api" { - if len(location) <= 6 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - triggerId = location[6] - } - - if len(workflowId) == 0 || len(triggerId) == 0 { - log.Printf("Ids can't be zero when deleting %s", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - ctx := context.Background() - workflow, err := shuffle.GetWorkflow(ctx, workflowId) - if err != nil { - log.Printf("[WARNING] Failed getting the workflow locally (delete outlook): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("[WARNING] Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Id != workflow.Owner || len(user.Id) == 0 { - if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" { - log.Printf("[INFO] User %s is accessing %s as admin (delete gmail sub)", user.Username, workflow.ID) - } else { - log.Printf("[WARNING] Wrong user (%s) for workflow %s when deleting gmail ", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - } - - trigger, err := shuffle.GetTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("[WARNING] Wrong user (%s) for workflow %s when deleting gmail (2)", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - err = handleGmailSubRemoval(ctx, user, workflowId, triggerId) - if err == nil { - // FIXME: Actually delete the hook, not just the data - shuffle.DeleteKey(ctx, "gmail_subscription", trigger.SubscriptionId) - shuffle.DeleteKey(ctx, "trigger_auth", triggerId) - } else { - log.Printf("[WARNING] Failed deleting gmail sub: %s", err) - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - var triggerId string - if location[1] == "api" { - if len(location) <= 6 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - triggerId = location[6] - } - - if len(workflowId) == 0 || len(triggerId) == 0 { - log.Printf("Ids can't be zero when deleting %s", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - ctx := context.Background() - workflow, err := shuffle.GetWorkflow(ctx, workflowId) - if err != nil { - log.Printf("[WARNING] Failed getting the workflow locally (delete outlook): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("[WARNING] Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != workflow.Owner || len(user.Id) == 0 { - if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" { - log.Printf("[INFO] User %s is accessing %s as admin (delete outlook sub)", user.Username, workflow.ID) - } else { - log.Printf("[WARNING] Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - } - - // Check what kind of sub it is - err = handleOutlookSubRemoval(ctx, user, workflowId, triggerId) - if err != nil { - log.Printf("Failed sub removal: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -// This sets up the sub with outlook itself -// Parses data from the workflow to see whether access is right to subscribe it -// Creates the cloud function for outlook return -// Wait for it to be available, then schedule a workflow to it -func handleCreateOutlookSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - ctx := context.Background() - workflow, err := shuffle.GetWorkflow(ctx, workflowId) - if err != nil { - log.Printf("[WARNING] Failed getting the workflow locally (outlook sub): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != workflow.Owner && user.Role != "admin" { - log.Printf("[WARNING] Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Println("[INFO] Handle outlook subscription for trigger") - - // Should already be authorized at this point, as the workflow is shared - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Failed body read for workflow %s", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Based on the input data from frontend - type CurTrigger struct { - Name string `json:"name"` - Folders []string `json:"folders"` - ID string `json:"id"` - } - - //log.Println(string(body)) - var curTrigger CurTrigger - err = json.Unmarshal(body, &curTrigger) - if err != nil { - log.Printf("Failed body read unmarshal for trigger %s", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if len(curTrigger.Folders) == 0 { - log.Printf("Error for %s. Choosing folders is required, currently 0", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Now that it's deployed - wait a few seconds before generating: - // 1. Oauth2 token thingies for outlook.office.com - // 2. Set the url to have the right mailboxes (probably ID?) ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages") - // 3. Set the callback URL to be the new trigger - // 4. Run subscription test - // 5. Set the subscriptionId to the trigger object - - // First - lets regenerate an oauth token for outlook.office.com from the original items - trigger, err := shuffle.GetTriggerAuth(ctx, curTrigger.ID) - if err != nil { - log.Printf("[INFO] Trigger %s doesn't exist - gmail sub.", curTrigger.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - // url doesn't really matter here - //url := fmt.Sprintf("https://shuffler.io") - redirectDomain := "localhost:5001" - url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) - outlookClient, _, err := shuffle.GetOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("[WARNING] Oauth client failure for gmail - triggerauth: %s", err) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - resp.WriteHeader(401) - return - } - - // Location + - - // This is here simply to let the function start - // Usually takes 10 attempts minimum :O - // 10 * 5 = 50 seconds. That's waaay too much :( - - if runningEnvironment != "cloud" { - org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("Failed finding org %s: %s", org.Id, err) - return - } - log.Printf("[INFO] Starting cloud configuration TO START trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId) - - action := shuffle.CloudSyncJob{ - Type: "outlook", - Action: "start", - OrgId: org.Id, - PrimaryItemId: trigger.Id, - SecondaryItem: trigger.Start, - ThirdItem: workflowId, - } - - err = executeCloudAction(action, org.SyncConfig.Apikey) - if err != nil { - log.Printf("[INFO] Failed cloud action START outlook execution: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } else { - log.Printf("[INFO] Successfully set up cloud action trigger") - } - } else { - log.Printf("Should configure a running environment for CLOUD") - } - - notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) - curSubscriptions, err := getOutlookSubscriptions(outlookClient) - if err == nil { - for _, sub := range curSubscriptions.Value { - if sub.NotificationURL == notificationURL { - log.Printf("[INFO] Removing existing subscription %s", sub.Id) - removeOutlookSubscription(outlookClient, sub.Id) - } - } - } else { - log.Printf("[INFO] Failed to get subscriptions - need to overwrite") - } - - maxFails := 5 - failCnt := 0 - log.Println("[INFO] Folders: %#v", curTrigger.Folders) - for { - subId, err := makeOutlookSubscription(outlookClient, curTrigger.Folders, notificationURL) - if err != nil { - failCnt += 1 - - log.Printf("[WARNING] Failed making oauth subscription for outlook, retrying in 5 seconds: %s", err) - - time.Sleep(5 * time.Second) - if failCnt == maxFails { - log.Printf("Failed to set up subscription %d times.", maxFails) - resp.WriteHeader(401) - return - } - - continue - } - - // Set the ID somewhere here - trigger.SubscriptionId = subId - err = shuffle.SetTriggerAuth(ctx, *trigger) - if err != nil { - log.Printf("[WARNING] Failed setting triggerauth (gmail): %s", err) - } - - break - } - - log.Printf("[INFO] Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) - - //log.Printf("%#v", user) - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -// This sets up the sub with outlook itself -// Parses data from the workflow to see whether access is right to subscribe it -// Creates the cloud function for outlook return -// Wait for it to be available, then schedule a workflow to it -func handleCreateGmailSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - ctx := context.Background() - workflow, err := shuffle.GetWorkflow(ctx, workflowId) - if err != nil { - log.Printf("[WARNING] Failed getting the workflow locally (outlook sub): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := shuffle.HandleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != workflow.Owner && user.Role != "admin" { - log.Printf("[WARNING] Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - log.Println("[INFO] Handle gmail subscription for trigger!") - - // Should already be authorized at this point, as the workflow is shared - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("Failed body read for workflow %s", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Based on the input data from frontend - type CurTrigger struct { - Name string `json:"name"` - Folders []string `json:"folders"` - ID string `json:"id"` - } - - //log.Println(string(body)) - var curTrigger CurTrigger - err = json.Unmarshal(body, &curTrigger) - if err != nil { - log.Printf("Failed body read unmarshal for trigger %s", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if len(curTrigger.Folders) == 0 { - log.Printf("[WARNING] Error for %s. Choosing folders is required, currently 0", workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Now that it's deployed - wait a few seconds before generating: - // 1. Oauth2 token thingies for outlook.office.com - // 2. Set the url to have the right mailboxes (probably ID?) ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages") - // 3. Set the callback URL to be the new trigger - // 4. Run subscription test - // 5. Set the subscriptionId to the trigger object - - // First - lets regenerate an oauth token for outlook.office.com from the original items - trigger, err := shuffle.GetTriggerAuth(ctx, curTrigger.ID) - if err != nil { - log.Printf("[INFO] Trigger %s doesn't exist - gmail sub.", curTrigger.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - // url doesn't really matter here - //url := fmt.Sprintf("https://shuffler.io") - redirectDomain := "localhost:5001" - url := fmt.Sprintf("http://%s/api/v1/triggers/gmail/register", redirectDomain) - gmailClient, _, err := shuffle.GetGmailClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("[WARNING] Oauth client failure in gmail - triggerauth: %s", err) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - resp.WriteHeader(401) - return - } - - // Location + - - // This is here simply to let the function start - // Usually takes 10 attempts minimum :O - // 10 * 5 = 50 seconds. That's waaay too much :( - - /* - if runningEnvironment != "cloud" { - org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) - if err != nil { - log.Printf("Failed finding org %s: %s", org.Id, err) - return - } - log.Printf("[INFO] Starting cloud configuration TO START trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId) - - action := shuffle.CloudSyncJob{ - Type: "gmail", - Action: "start", - OrgId: org.Id, - PrimaryItemId: trigger.Id, - SecondaryItem: trigger.Start, - ThirdItem: workflowId, - } - - err = executeCloudAction(action, org.SyncConfig.Apikey) - if err != nil { - log.Printf("[INFO] Failed cloud action START gmail execution: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } else { - log.Printf("[INFO] Successfully set up cloud action trigger") - } - } else { - log.Printf("Should configure a running environment for CLOUD") - } - */ - - notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) - //curSubscriptions, err := getOutlookSubscriptions(outlookClient) - //if err == nil { - // for _, sub := range curSubscriptions.Value { - // if sub.NotificationURL == notificationURL { - // log.Printf("[INFO] Removing existing subscription %s", sub.Id) - // removeOutlookSubscription(outlookClient, sub.Id) - // } - // } - //} else { - // log.Printf("[INFO] Failed to get subscriptions - need to overwrite") - //} - - //maxFails := 5 - //failCnt := 0 - log.Printf("Starting with notificationURL %s", notificationURL) - log.Println("[INFO] Folders: %#v", curTrigger.Folders) - for { - sub, err := makeGmailSubscription(gmailClient, curTrigger.Folders, notificationURL) - if err != nil { - //failCnt += 1 - - log.Printf("[WARNING] Failed making oauth subscription for gmail, retrying in 5 seconds: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - - //time.Sleep(5 * time.Second) - //if failCnt == maxFails { - // log.Printf("Failed to set up subscription %d times.", maxFails) - // resp.WriteHeader(401) - // return - //} - - //continue - } - - // May not need a new one; could just do this in triggerId with search tbh - newSub := shuffle.SubscriptionRecipient{ - HistoryId: sub.HistoryId, - TriggerId: trigger.Id, - Edited: 0, - Expiration: sub.Expiration, - LastSync: int(time.Now().Unix()), - } - err = shuffle.SetSubscriptionRecipient(ctx, newSub, newSub.HistoryId) - if err != nil { - log.Printf("[WARNING] Failed setting sub ID (gmail): %s", sub.HistoryId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Set the ID somewhere here - trigger.SubscriptionId = sub.HistoryId - err = shuffle.SetTriggerAuth(ctx, *trigger) - if err != nil { - log.Printf("[WARNING] Failed setting triggerauth (gmail): %s", err) - } - - break - } - - log.Printf("[INFO] Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) - - //log.Printf("%#v", user) - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func handleGmailRouting(resp http.ResponseWriter, request *http.Request) { - body, err := ioutil.ReadAll(request.Body) - if err != nil { - log.Printf("[WARNING] Body read error for gmail message: %s", err) - resp.WriteHeader(200) - return - } - - parsedMessage := shuffle.Inputdata{} - err = json.Unmarshal(body, &parsedMessage) - if err != nil { - log.Printf("[WARNING] Unmarshal error for gmail message: %s", err) - resp.WriteHeader(200) - return - } - - log.Printf("GMAIL BODY:\n%s", string(body)) - parsedData, err := base64.StdEncoding.DecodeString(parsedMessage.Message.Data) - if err != nil { - log.Printf("[WARNING] Failed base64 decode in gmail routing: %s", err) - resp.WriteHeader(200) - return - } - - log.Printf("Parsed data: %s", string(parsedData)) - findHistory := shuffle.ParsedMessage{} - err = json.Unmarshal(parsedData, &findHistory) - if err != nil { - log.Printf("[WARNING] Unmarshal error for gmail message (2): %s", err) - resp.WriteHeader(200) - return - } - - // This History ID will match the ID that is received when the subscription is configured. - log.Printf("HistoryId: %d. Email: %s", findHistory.HistoryId, findHistory.EmailAddress) - ctx := context.Background() - subscription, err := shuffle.GetSubscriptionRecipient(ctx, fmt.Sprintf("%d", findHistory.HistoryId)) - if err != nil { - log.Printf("[WARNING] Failed finding gmail history for ID %d: %s. Should the subscription be cancelled?", findHistory.HistoryId, err) - resp.WriteHeader(200) - return - } - - log.Printf("SUB: %#v", subscription) - - resp.WriteHeader(200) -} diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 4b9756cb..aaf6e2a6 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1080,9 +1080,14 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { // log.Printf("Failed to delete webhook: %s", err) //} } else if item.TriggerType == "EMAIL" { - err = handleOutlookSubRemoval(ctx, user, workflow.ID, item.ID) + err = shuffle.HandleOutlookSubRemoval(ctx, user, workflow.ID, item.ID) if err != nil { - log.Printf("Failed to delete email sub: %s", err) + log.Printf("Failed to delete OUTLOOK email sub (checking gmail after): %s", err) + } + + err = shuffle.HandleGmailSubRemoval(ctx, user, workflow.ID, item.ID) + if err != nil { + log.Printf("Failed to delete gmail email sub: %s", err) } } diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index fb538fcb..a410d447 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -4125,18 +4125,32 @@ const AngularWorkflow = (props) => { "long_description": "Create a schedule based on cron", }, { - "name": "Email", + "name": "Office365", "type": "TRIGGER", "status": "uninitialized", - "description": "Add your email provider", + "description": "Starts upon O365 email", "trigger_type": "EMAIL", "errors": null, "is_valid": cloudSyncEnabled || isCloud ? true : false, "label": "Email", "environment": "cloud", - "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoM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', + "large_image": 'data:image/jpg;base64,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', "long_description": "Execute a workflow when you get an email", - }] + }, + { + "name": "Gmail", + "type": "TRIGGER", + "status": "uninitialized", + "description": "Trigger based on Gmail", + "trigger_type": "EMAIL", + "errors": null, + "is_valid": cloudSyncEnabled || isCloud ? true : false, + "label": "Email", + "environment": "cloud", + "large_image": 'data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAzFodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QTIyMjgyMEYwMDJDMTFFQkJBOEE5OUJBM0MzMTA2RDIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QTIyMjgyMTAwMDJDMTFFQkJBOEE5OUJBM0MzMTA2RDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozQTMwMDQxRTAwMEUxMUVCQkE4QTk5QkEzQzMxMDZEMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBMjIyODIwRTAwMkMxMUVCQkE4QTk5QkEzQzMxMDZEMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAwICAwMCAwMDAwMEBwUEBAQECQYHBQcKCQsLCgkKCgwNEQ4MDBAMCgoOFA8QERITExMLDhQWFBIWERITEv/bAEMBAwMDBAQECAUFCBIMCgwSEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEv/AABEIAK4ArgMBEQACEQEDEQH/xAAcAAEAAQUBAQAAAAAAAAAAAAAABgEFBwgJBAP/xABBEAABAwIDBAUGCwgDAAAAAAAAAQIEAwUGBxESITFBCDdRYXUTFDJScbMYIiM2QmJ0gcHD0QkzNVSRlbHCcpLw/8QAHAEBAAIDAQEBAAAAAAAAAAAAAAYHBAUIAwIB/8QAQBEAAgECAgYGBwUHBAMAAAAAAAECAwQFEQYhMUFRYQcSMjRxciI1UqGx0fATM4GRshQWYpLBwuFCotLxFRck/9oADAMBAAIRAxEAPwDpgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiarom9V4AGo+enTspYNxBMw/lZbYN5k2+o6jLus57ljtqtXRzKTGKi1NldUVyuRNUXRF4kevscVKbhRWeW97PwLf0X6LpXtvG6xGbgpLNRjl1stzk3nlnwyz45EOy9/aGXmldqNHNGxW2VbKrkSpLs9N9GvQT1vJuc5tRE7EVq9irwMa30gn1sq0VlyNzi/RHaui5YdWkprdPJp8s0k1460bpYUxbZ8cWGLesJXGNdLXMbrRkx3atVebVTi1ycFaqIqc0JNSqwqwU4PNMpO/sLmwuJW9zBwnHan9a1wa1Mu56GGAAAAAAACz4kxVBwxG8pOftVnp8lHZvfU/RO9SJ6V6Z4Zo5b/AGl1LOb7MF2pfJcZPVwzeo2mF4Rc4hU6tJaltb2L5vkY8rZvXV1faoRYNOlrupua5y6f8tU/wULX6ccdlX61KhTjDg1Jv8ZdZe5Im8NDLJQylOTfHUvdkTXB+OI+KmPpOp+bTaTdp9Ha1RzfWavZ2pyLi0F6Q7TSaMqTj9nXis3HPNNe1F8OKetc1rInjWAVcOakn1oPfwfB/WskxYhoAAAAAAAAAAAACN5l3SvY8ucV3GA5WSoNjm16L0X0XtoPVq/cqIp43MnGjOS2pP4GywahCviVvSnslOCfg5I47s12G6qqrspqq8ytzsp7Sp+n4ZW6PWbOIsqsTyZWGZa+bV6SOmW+squjytHInx28nacHpo5PZuPahf1rOSnTfitz+uJocf0Zw/HLf7K6jrXZku1HwfDinqfvOi2U+dNgzat21Z6ixLrQZtS7XXenlaXa5vrs+sn3oik1w/FKF5H0NUt63/5RzZpPohiGA1sqy61N9ma2Pk/ZfJ/g2T82RFQAAAiarom9V7ADAWdvSkt+CvObNgN0e7X5urK0n040F3PXTdUqJ6qfFTmvIjuKY9ChnToelLjuXzZamh/RtcYl1brEM6dHalslP/jHnte5byOWi6zL5aIFwu8irLmy4lKrXrVF1c9ysRVVf68E3HFukt1XucXualablLry1t57G0vwS2LcSuta0bWrOjQiowi2kluWZ6zRnmX7Ald8fF9rWmqpt10pu72uRUVCcdG9xUoaU2UoPLOfVfhJNP3M02kFOM8MrJ7ln+Wszuh2winQAAAAAAAAAAACKZsx3y8q8ZUaOi1K2H5zGIq6JqtB6IP2Wpdf/PS7U/RW7W9S95m4be0bG9o3dbsU5RlLJZvKLTeS36lsOQdeJWg1VoS6b6VWmiI5jk0VCvb/AA+6sLiVtdU3CpHant/64NanuOv8NxSyxO1heWVVVKU9alF5p/JrenrW9HzMQzySYE/icj7P/sh4V+yj6iZBtd1mWS4x59nlV4U2I9H0JFB6sfTd2oqf+U8KdSdOSlB5NHnc21G5oyo1oKUJamms0zb3JPpVw8T+b2XMmpHtt3doyhctEpx5a8ER/Kk9f+q/V4E1wvSCFXKncapcdz+T9xQWmHRnWsutd4WnOltcNso+HtL/AHLntNiSTFRnivN6gYdtci5X2ZHgQIjNuvIrv2WMT29vYib15HnVqwpQc5vJIybSzuLuvGhbwc5y2Ja2/rjsW807zs6Us/GSSLLgB0i02J2rK0z0JM1vNO2nTXsT4y81TgQnFMenXzp0NUeO9/Je86C0P6NbfDurdYjlUrbVHbGP/KXPYty3mv8Apo3RNyIhHEWtvNp8J/Naz/YKHu0OYMc9Z3Pnl+plSYh3ur5n8S6mrMQ9mGb1Dt2N8OxpddrZE64U6dCkm9z1XXfp2d5YPRnhV3d6RWtalDOFOacnuWXPi9y2kU0rxuxsrR29eolUq+jCO9t8uC3vZ+JsSnA7PRV4AAAAAAAAAAABHcxur3E/g0v3LjaYJ6zt/PH9SMPEe51fK/gzmxecPxL9FayYzSo1vydZvpM/VO4vfSnRDDdIaH2d1HKa7M12o/NcYvU+T1kP0L08xjRS6+2sZ5wl26cuxPxW6XCS1rmtRjK+4cl2Cvsym7dFy/J12p8V36L3HKelWhuJaO1+pcxzpvszXZl8pfwv8M1rO3dCOkHB9LLbr2curVivTpy7Uef8UeElq45PUe/An8TkfZ/9kIZX7KJ5Em5jH2OKaLwUAzdk70oLxl3GbasS0q9/sdJipGYtVEkRVRNzWPdxZru2XcOS8jfYbj1W1XUqLrR3cV/grbSzo4s8Xn+0WrVKs3r1ejLi2lslzW3fxITmlnDiDNm6JXv9ZKECg5Vh22g5UoR+/wCu/teu/s0TcYF/iVe8nnUepbFuX+eZJNG9FMPwGh1LeOc32pvtS+S5LVxzesg5gElC8F9gBtNhP5rWf7BQ92hzBjnrO588/wBTKlxDvdXzP4kZxrmfGsXlIdl8nLuCao52utOgvf2u7v69hNtEuj25xPq3N7nTo7l/ql4cFze3ct5TGm/Sla4R1rTD8qlfY3tjDx9qX8K2b3uIllBcJN0zrwlKuNapIkVbxSV1R66qvHd3J3IdE4JY29lKjb20FGEXqS+tb4t62c+YfiF1iGO0bm6qOdSU1m39aktyWpbjfxOCE9LkKgAAAAAAAAAAAjuY3V7ifwaX7lxtME9Z2/nj+pGHiPc6vlfwZzrb6LfYh0+9pTZ85EalLoPoyqbKtKomjmPTVFMW8sre9oSt7mCnCWpprNP6/NbjMw/ELvD7mF1aVHTqQealF5NP62rY9jLfhXKe6S7hdpWEote4x4EHziTQpptVaNPbaiuROL0TXfpvRO05b6R+jh4Ild2MutRk8uq9covLPb/qjz2rfntOx+jDplo47lYYslTuEtU1qhPdr9iXLsvdlsPjx4bynS/QAAAAAD2WizzsQXKPbrHEkTp8x+xQj0GK99R3cn+V4JzPulTnUmoQWbZ4XV1QtaMq9eajCOtt6kjIuKsa3O2RW4ZjsdAqWmmkKc9r0V76tNNh7WuTcjdUVNU3qRjCej23tsQq3t/lObnJqO2Mdbaz9p+5c9pwj0k9Ktxf3lxZYW3TpdaSc9k5a3s9mP8AufLYQMsQowm+SPXBg7xel+JlWPeqfibjR/1rb+ZHQNOCEzLwKgAAAAAAAAAAAjuY3V7ifwaX7lxtME9Z2/nj+pGHiPc6vlfwZzrb6LfYh0+9pTZUAz50N92Pr4qblSzfnsK26TfV1Hz/ANrJZof3up5f6oyFnZ0X7bjvzi8YKSPaMQu1fVpabEac76yJ+7evrpuX6ScznTFMBp3GdSj6M/c/k+f5nSmh/SPc4X1bW+zqUNie2UPD2o8nrW57jTa/YfuWF7tItmIYUi33CK7Zqx67dlzexexUXkqaovJSD1qNSjNwqLJo6Gsr62vaEbi2mpwlsa+tvFPWi3nmZQAJpljlJiDNa7LFw5HRkWi5EmXCuipQjIvav0ndjE3r3JvM6xw+veT6tNat73L64Ed0j0ow/AqH2l1L0n2YrtS8OC4t6l46jeLKnJrD+U1uSlY6SybjXaiS7nIanlq/cnqM1+in36rvJ9h+GULKOUFm973v5Lkc06S6W4hj1brV31aa7MF2Vz5vm/wyRpFmV1jYq8al++cRq6+/n4v4nL2K9/r+aXxZGzwMAm+SPXBg7xel+JlWPeqfibjR/wBa2/mR0DTghMy8CoAAAAAAAAAAAI7mN1e4n8Gl+5cbTBPWdv54/qRh4j3Or5X8Gc62+i32IdPvaU2VAM+dDj5+33wb89hW3Sb6uo+f+1ks0P75U8v9UbclKliEMzNylw/mtaUiYkjqyVRaqQ7hQRErxVX1V5t7WLuXuXeYN9h1C8h1ai17nvX1wJFo7pRiGBV/tLWXovtRfZl48Hwa1rw1Gj2a2TV/ykuPk75SSRbKz1SJdKDV8jW7l9R+nFq/cqpvIBiGGV7KWU9cdz3f4fI6W0Z0tw/HqPWt3lUXag+0vmua/HJk8yT6L1yx15vecbpItGH3aPpUdNiTOb9VF/dsX1l3r9FOZscLwGpcZVK3ow97+S5/kRfTDpItsL61rYZVK+xvbGHj7UuS1Le9xuTYbBbsL2mPbMPQo9vt8RuzRj0GbLW9q96rzVdVXmpOKNGnRgoU1kkc83t9c3teVxczc5y2t7f+uCWpFwb6Se09DFOc+ZXWNirxqX75xCbr7+fi/iUNivf6/ml8WRs8DAJvkj1wYO8XpfiZVj3qn4m40f8AWtv5kdA04ITMvAqAAAAAAAAAAACO5jdXuJ/BpfuXG0wT1nb+eP6kYeI9zq+V/BnOtvot9iHT72lNlQDPnQ4+ft98G/PYVt0m+rqPn/tZLND++VPL/VG3JSpYgAPjMhR7jHdHuEehKoPVFdSr00qMcqLqiq1UVNyoiofMoxkspLNHpSrVKM1OnJxa3p5P80fZV1XVd6n0eYAKt9JPaAc58yusbFXjUv3ziE3X38/F/EobFe/1/NL4sjZ4GATfJHrgwd4vS/EyrHvVPxNxo/61t/MjoGnBCZl4FQAAAAAAAAAAAWbGlvrXbB19gwm7ciZbJNGi31nupORqfeqohnYXWhRvqNWeyMot+CaMa9pyqW1SEdri17jnHsuZ8V7Va5u5zVTRUVNyovedS5p60UwADPnQ4+ft98G/PYVt0m+rqPn/ALWSzQ/vlTy/1RtyUqWIAAAAAAVb6Se0A5z5ldY2KvGpfvnEJuvv5+L+JQ2K9/r+aXxZGzwMAyBkFbZFzzjwq2HTc9Y05JNVUTcynTarnOXu4J7VQzMPi5XUMuJvNG6UqmK0FFbHm/BbTftOCExLsAAAAAAAAAAAAABjvFWQGB8YXSrcrtaH0psh21XqwpL4/lXc3Oa3cq9+mq8yT4fpjjFjRVGlVzitiklLLks9eRprrALC5qOpOGt7cm1mWb4K2Xn8jdf7rUM//wBg477cf5EY37rYb7L/AJmSbAeTWF8t7lJn4UjTaMmVH8hUWvMdWRWbSO3IvBdUTeanF9J8RxWlGldSTinmsopa8sjOscGtLKbnRTTay1vMm5HzaAAAAAABF0XUAxVd+jLgO+XWZcbhDubpU+Q+RXcy5Paive5XO0TkmqruNfPC7acnJp5vmRqtonhlarKpOLzk236T2s8nwUcu/wCRu391qHx/4m14P8zz/c3CfZl/MybYGyvwzlxSrNwjbGRKshEStIe91WtUROCK9yqunPRNEMuha0aH3ayNvh+EWdgn+zwyb2va/wA2SoyDZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//2Q==', + "long_description": "Execute a workflow when you get an email", + } + ] const TriggersView = () => { const triggersViewStyle = { @@ -5700,17 +5714,24 @@ const AngularWorkflow = (props) => { } - const gmailButton = + const gmailButton = selectedTrigger.name !== "Gmail" ? null : - const outlookButton = + const outlookButton = selectedTrigger.name !== "Office365" ? null : @@ -5844,7 +5877,7 @@ const AngularWorkflow = (props) => {
- Select a folder + Select folders