Skip to content

Commit 0eb0986

Browse files
authored
feat: Outlook Mail Tool to support Group Mailboxes (#521)
* feat: add tool to list outlook groups * feat: add list group mail tool * feat: add createGroupMessage (need to support cc and bcc) * fix: list group only returns the groups that user is a member of * fix: better naming list group thread method * feat: support delete Group Thread * fix: improve print messages * enhance: support attachments * fix: update tool descriptopn, clean up code. * feat: support reply-to-thread * fix: clean up
1 parent 1df2ace commit 0eb0986

File tree

9 files changed

+513
-8
lines changed

9 files changed

+513
-8
lines changed

outlook/mail/credential/tool.gpt

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Tools: ../../../oauth2
1818
"Mail.Send",
1919
"User.Read",
2020
"MailboxSettings.Read",
21+
"Group.Read.All",
22+
"Group.ReadWrite.All",
2123
"offline_access"
2224
]
2325
}

outlook/mail/main.go

+39-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ func main() {
3535
fmt.Printf("failed to list mail: %v\n", err)
3636
os.Exit(1)
3737
}
38+
case "listGroupThreads":
39+
if err := commands.ListGroupThreads(
40+
context.Background(),
41+
os.Getenv("GROUP_ID"),
42+
os.Getenv("START"),
43+
os.Getenv("END"),
44+
os.Getenv("LIMIT"),
45+
); err != nil {
46+
fmt.Printf("failed to list group threads: %v\n", err)
47+
os.Exit(1)
48+
}
49+
case "listGroups":
50+
if err := commands.ListGroups(context.Background()); err != nil {
51+
fmt.Printf("failed to list groups: %v\n", err)
52+
os.Exit(1)
53+
}
3854
case "getMessageDetails":
3955
if err := commands.GetMessageDetails(context.Background(), os.Getenv("MESSAGE_ID")); err != nil {
4056
fmt.Printf("failed to get message details: %v\n", err)
@@ -59,6 +75,11 @@ func main() {
5975
fmt.Printf("failed to create draft: %v\n", err)
6076
os.Exit(1)
6177
}
78+
case "createGroupThreadMessage":
79+
if err := commands.CreateGroupThreadMessage(context.Background(), os.Getenv("GROUP_ID"), os.Getenv("REPLY_TO_THREAD_ID"), getDraftInfoFromEnv()); err != nil {
80+
fmt.Printf("failed to create group thread message: %v\n", err)
81+
os.Exit(1)
82+
}
6283
case "sendDraft":
6384
if err := commands.SendDraft(context.Background(), os.Getenv("DRAFT_ID")); err != nil {
6485
fmt.Printf("failed to send draft: %v\n", err)
@@ -69,6 +90,11 @@ func main() {
6990
fmt.Printf("failed to delete message: %v\n", err)
7091
os.Exit(1)
7192
}
93+
case "deleteGroupThread":
94+
if err := commands.DeleteGroupThread(context.Background(), os.Getenv("GROUP_ID"), os.Getenv("THREAD_ID")); err != nil {
95+
fmt.Printf("failed to delete group thread: %v\n", err)
96+
os.Exit(1)
97+
}
7298
case "moveMessage":
7399
if err := commands.MoveMessage(context.Background(), os.Getenv("MESSAGE_ID"), os.Getenv("DESTINATION_FOLDER_ID")); err != nil {
74100
fmt.Printf("failed to move message: %v\n", err)
@@ -95,18 +121,27 @@ func main() {
95121
}
96122
}
97123

124+
125+
func smartSplit(s, sep string) []string {
126+
if s == "" {
127+
return []string{} // Return an empty slice if the input is empty
128+
}
129+
return strings.Split(s, sep)
130+
}
131+
132+
98133
func getDraftInfoFromEnv() graph.DraftInfo {
99134
var attachments []string
100135
if os.Getenv("ATTACHMENTS") != "" {
101-
attachments = strings.Split(os.Getenv("ATTACHMENTS"), ",")
136+
attachments = smartSplit(os.Getenv("ATTACHMENTS"), ",")
102137
}
103138

104139
info := graph.DraftInfo{
105140
Subject: os.Getenv("SUBJECT"),
106141
Body: os.Getenv("BODY"),
107-
Recipients: strings.Split(os.Getenv("RECIPIENTS"), ","),
108-
CC: strings.Split(os.Getenv("CC"), ","),
109-
BCC: strings.Split(os.Getenv("BCC"), ","),
142+
Recipients: smartSplit(os.Getenv("RECIPIENTS"), ","),
143+
CC: smartSplit(os.Getenv("CC"), ","),
144+
BCC: smartSplit(os.Getenv("BCC"), ","),
110145
Attachments: attachments,
111146
}
112147

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/gptscript-ai/tools/outlook/mail/pkg/client"
8+
"github.com/gptscript-ai/tools/outlook/mail/pkg/global"
9+
"github.com/gptscript-ai/tools/outlook/mail/pkg/graph"
10+
"github.com/gptscript-ai/tools/outlook/mail/pkg/util"
11+
)
12+
13+
func CreateGroupThreadMessage(ctx context.Context, groupID, replyToThreadID string, info graph.DraftInfo) error {
14+
c, err := client.NewClient(global.AllScopes)
15+
if err != nil {
16+
return fmt.Errorf("failed to create client: %w", err)
17+
}
18+
19+
if replyToThreadID != "" { // reply to a thread
20+
err = graph.ReplyToGroupThreadMessage(ctx, c, groupID, replyToThreadID, info)
21+
if err != nil {
22+
return fmt.Errorf("failed to reply to group thread message: %w", err)
23+
}
24+
fmt.Println("Group thread message replied to successfully")
25+
return nil
26+
} else { // create a new thread
27+
threads, err := graph.CreateGroupThreadMessage(ctx, c, groupID, info)
28+
if err != nil {
29+
return fmt.Errorf("failed to create group thread message: %w", err)
30+
}
31+
32+
fmt.Println("Group thread message created successfully, thread ID:", util.Deref(threads.GetId()))
33+
return nil
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
graph "github.com/gptscript-ai/tools/outlook/mail/pkg/graph"
8+
"github.com/gptscript-ai/tools/outlook/mail/pkg/client"
9+
"github.com/gptscript-ai/tools/outlook/mail/pkg/global"
10+
)
11+
12+
func DeleteGroupThread(ctx context.Context, groupID, threadID string) error {
13+
c, err := client.NewClient(global.AllScopes)
14+
if err != nil {
15+
return fmt.Errorf("failed to create client: %w", err)
16+
}
17+
18+
if err := graph.DeleteGroupThread(ctx, c, groupID, threadID); err != nil {
19+
return err
20+
}
21+
22+
fmt.Printf("Group thread %s deleted successfully\n", threadID)
23+
return nil
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/gptscript-ai/tools/outlook/mail/pkg/client"
9+
"github.com/gptscript-ai/tools/outlook/mail/pkg/global"
10+
"github.com/gptscript-ai/tools/outlook/mail/pkg/graph"
11+
"github.com/gptscript-ai/tools/outlook/mail/pkg/util"
12+
md "github.com/JohannesKaufmann/html-to-markdown"
13+
)
14+
15+
16+
17+
func ListGroupThreads(ctx context.Context, groupID, start, end, limit string) error {
18+
var (
19+
limitInt int = 100
20+
err error
21+
)
22+
if limit != "" {
23+
limitInt, err = strconv.Atoi(limit)
24+
if err != nil {
25+
return fmt.Errorf("failed to parse limit: %w", err)
26+
}
27+
if limitInt < 1 {
28+
return fmt.Errorf("limit must be a positive integer")
29+
}
30+
}
31+
32+
if groupID == "" {
33+
return fmt.Errorf("group ID is required")
34+
}
35+
36+
c, err := client.NewClient(global.ReadOnlyScopes)
37+
if err != nil {
38+
return fmt.Errorf("failed to create client: %w", err)
39+
}
40+
41+
threads, err := graph.ListGroupThreads(ctx, c, groupID, start, end, limitInt)
42+
if err != nil {
43+
return fmt.Errorf("failed to list group threads: %w", err)
44+
}
45+
46+
for _, thread := range threads {
47+
threadID := util.Deref(thread.GetId())
48+
49+
fmt.Printf("📩 Thread ID: %s\n", threadID)
50+
if thread.GetTopic() != nil {
51+
fmt.Printf("📌 Subject: %s\n", util.Deref(thread.GetTopic()))
52+
} else {
53+
fmt.Println("📌 Subject: (No Subject)")
54+
}
55+
fmt.Printf("📅 Last Delivered: %s\n", thread.GetLastDeliveredDateTime().String())
56+
57+
// Print unique senders
58+
senders := thread.GetUniqueSenders()
59+
fmt.Print("👥 Unique Senders: ")
60+
for _, sender := range senders {
61+
fmt.Printf("%s, ", sender)
62+
}
63+
fmt.Println()
64+
65+
// Fetch posts (individual emails/messages) inside the thread and then print them
66+
posts, err := graph.ListThreadMessages(ctx, c, groupID, threadID)
67+
if err != nil {
68+
return fmt.Errorf("failed to list thread messages: %w", err)
69+
}
70+
71+
fmt.Println("\n✉️ Messages:")
72+
for i, post := range posts {
73+
messageID := util.Deref(post.GetId())
74+
fmt.Printf("📧 Message %d, ID: %s\n", i+1, messageID)
75+
76+
// Check if sender information is available
77+
if post.GetFrom() != nil && post.GetFrom().GetEmailAddress() != nil {
78+
fmt.Printf("👤 From: %s <%s>\n",
79+
util.Deref(post.GetFrom().GetEmailAddress().GetName()),
80+
util.Deref(post.GetFrom().GetEmailAddress().GetAddress()),
81+
)
82+
} else {
83+
fmt.Println("👤 Sender: Unknown")
84+
}
85+
86+
fmt.Printf("📅 Sent: %s\n", post.GetReceivedDateTime().String())
87+
88+
// Print message body if available
89+
if post.GetBody() != nil && post.GetBody().GetContent() != nil {
90+
fmt.Println("📝 Message Body:")
91+
converter := md.NewConverter("", true, nil)
92+
bodyHTML := util.Deref(post.GetBody().GetContent())
93+
bodyMarkdown, err := converter.ConvertString(bodyHTML)
94+
if err != nil {
95+
return fmt.Errorf("failed to convert email body HTML to markdown: %w", err)
96+
}
97+
fmt.Println(bodyMarkdown)
98+
99+
} else {
100+
fmt.Println("📭 (No content in this message)")
101+
}
102+
fmt.Println()
103+
}
104+
105+
fmt.Println("\n")
106+
}
107+
return nil
108+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/gptscript-ai/tools/outlook/mail/pkg/client"
8+
"github.com/gptscript-ai/tools/outlook/mail/pkg/global"
9+
"github.com/gptscript-ai/tools/outlook/mail/pkg/graph"
10+
"github.com/gptscript-ai/tools/outlook/mail/pkg/util"
11+
)
12+
13+
func ListGroups(ctx context.Context) error {
14+
c, err := client.NewClient(global.ReadOnlyScopes)
15+
if err != nil {
16+
return fmt.Errorf("failed to create client: %w", err)
17+
}
18+
19+
groups, err := graph.ListGroups(ctx, c)
20+
if err != nil {
21+
return fmt.Errorf("failed to list groups: %w", err)
22+
}
23+
24+
if len(groups) == 0 {
25+
fmt.Println("No groups found")
26+
return nil
27+
}
28+
29+
if err != nil {
30+
return fmt.Errorf("failed to create GPTScript client: %w", err)
31+
}
32+
33+
for _, group := range groups {
34+
fmt.Printf("ID: %s\nName: %s\nDescription: %s\nMail: %s\n",
35+
util.Deref(group.GetId()),
36+
util.Deref(group.GetDisplayName()),
37+
util.Deref(group.GetDescription()),
38+
util.Deref(group.GetMail()),
39+
)
40+
}
41+
42+
return nil
43+
}

outlook/mail/pkg/global/global.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ package global
33
const CredentialEnv = "GPTSCRIPT_GRAPH_MICROSOFT_COM_BEARER_TOKEN"
44

55
var (
6-
ReadOnlyScopes = []string{"Mail.Read", "User.Read", "MailboxSettings.Read"}
7-
AllScopes = []string{"Mail.Read", "Mail.ReadWrite", "Mail.Send", "User.Read", "MailboxSettings.Read"}
6+
ReadOnlyScopes = []string{"Mail.Read", "User.Read", "MailboxSettings.Read", "Groups.Read.All"}
7+
AllScopes = []string{"Mail.Read", "Mail.ReadWrite", "Mail.Send", "User.Read", "MailboxSettings.Read", "Groups.ReadWrite.All"}
88
)

0 commit comments

Comments
 (0)