0

New Email

by
Published today

Emits messages received in a folder (default `inbox`) since the last poll, oldest first. The first run sets the watermark to now and emits nothing. Polls because Microsoft Graph push needs a change-notification subscription renewed every few days against a public endpoint, which belongs in a Windmill native trigger; run this as a Scheduled Poll until then. Requires Mail.Read (covered by the default Mail.ReadWrite) (a shared mailbox needs Mail.Read.Shared, not a default).

Scriptยท trigger ms_outlook Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 2 hours ago
1
//native
2

3
import * as wmill from "windmill-client"
4

5
/**
6
 * New Email
7
 * Emits messages received in a folder (default `inbox`) since the last poll, oldest first. The first run sets the watermark to now and emits nothing. Polls because Microsoft Graph push needs a change-notification subscription renewed every few days against a public endpoint, which belongs in a Windmill native trigger; run this as a Scheduled Poll until then. Requires Mail.Read (covered by the default Mail.ReadWrite) (a shared mailbox needs Mail.Read.Shared, not a default).
8
 */
9
export async function main(
10
  auth: RT.MsOutlook,
11
  folder: string = "inbox",
12
  mailbox: string | undefined
13
) {
14
  // `seen` holds the ids already emitted for the watermark second, since
15
  // receivedDateTime only has second precision and the filter is inclusive.
16
  const state: { since: string; seen: string[] } | undefined =
17
    await wmill.getState()
18

19
  // First run: set the watermark to now and don't emit a backlog.
20
  if (!state?.since) {
21
    await wmill.setState({
22
      since: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
23
      seen: [],
24
    })
25
    return []
26
  }
27

28
  const base = `https://graph.microsoft.com/v1.0/${
29
    mailbox != null && mailbox !== ""
30
      ? `users/${encodeURIComponent(mailbox)}`
31
      : "me"
32
  }`
33
  const first = new URL(
34
    `${base}/mailFolders/${encodeURIComponent(folder)}/messages`
35
  )
36
  first.searchParams.append("$filter", `receivedDateTime ge ${state.since}`)
37
  first.searchParams.append("$orderby", "receivedDateTime asc")
38
  first.searchParams.append("$top", "100")
39

40
  const messages: { id: string; receivedDateTime: string }[] = []
41
  let next: string | undefined = first.toString()
42
  while (next) {
43
    const response = await fetch(next, {
44
      headers: {
45
        Authorization: `Bearer ${auth.token}`,
46
        Accept: "application/json",
47
      },
48
    })
49
    if (!response.ok) {
50
      throw new Error(`${response.status} ${await response.text()}`)
51
    }
52
    const page = (await response.json()) as {
53
      value: { id: string; receivedDateTime: string }[]
54
      "@odata.nextLink"?: string
55
    }
56
    messages.push(...page.value)
57
    next = page["@odata.nextLink"]
58
  }
59

60
  const fresh = messages.filter((m) => !state.seen.includes(m.id))
61
  if (fresh.length > 0) {
62
    const since = messages[messages.length - 1].receivedDateTime
63
    await wmill.setState({
64
      since,
65
      seen: messages
66
        .filter((m) => m.receivedDateTime === since)
67
        .map((m) => m.id),
68
    })
69
  }
70

71
  return fresh
72
}
73