eas

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: MIT Imports: 23 Imported by: 0

README

eas — Exchange ActiveSync 14.x client

Go Reference

A complete EAS (Microsoft Exchange ActiveSync) client implementation in Go. Targets protocol versions 12.0 / 12.1 / 14.0 / 14.1 / 16.0 / 16.1; tested against Z-Push and SOGo. Stdlib + one third-party dep (smallstep/pkcs7 for S/MIME).

For the broader project view (testenv, CI, contribution guide) see the repo root README. This page is the API tour.

Install

go get github.com/hstern/go-activesync/eas

Quick start

import (
    "context"
    "github.com/hstern/go-activesync/eas"
)

c, err := eas.NewClient(eas.Config{
    ServerURL: "https://mail.example.com/Microsoft-Server-ActiveSync",
    Username:  "henry",
    Password:  pw,                          // already pulled from your secret store
    DeviceID:  "32hexcharsofdeviceidhere00000000",
    State:     eas.NewMemoryState(),        // or your durable StateStore
})
if err != nil { return err }

ctx := context.Background()
_, _ = c.NegotiateVersion(ctx)              // pick the highest version both sides speak
if err := c.Provision(ctx); err != nil { return err }

folders, err := c.FolderSync(ctx)
for _, f := range folders.Added {
    fmt.Println(f.Type, f.DisplayName, f.ServerID)
}

The Provision handshake

Most EAS servers refuse every other command until the client has completed a two-phase Provision exchange and is sending X-MS-PolicyKey on every subsequent request. Client.Provision does both phases and persists the final key via your StateStore. On HTTP 449 from any later command, postRaw re-runs the whole dance and retries the original request once (MS-ASPROV §3.1.5.2) — you don't need to handle it manually.

Provision handshake

Sync state per folder

EAS Sync is per-folder and stateful. The first call to Sync on any new folder must use SyncKey="0" and is mandated by the spec to return zero items — only a fresh key. The second call returns the initial batch; subsequent calls return deltas. Client.SyncEmail walks that bootstrap transparently in one call. Pass EmailSyncOptions{NoBootstrap: true} if you want to observe each step (used in the test suite).

Sync state machine

State lives behind the StateStore interface. A concurrent-safe in-memory implementation (NewMemoryState()) ships for tests and one-shot CLIs; for production, plug in your own (bbolt, SQLite, Redis — anything with Get/Set semantics is a small adapter).

Command catalog

Class Symbols
Folders FolderSync, FolderCreate, FolderUpdate, FolderDelete
Email SyncEmail, FetchEmail, FetchAttachment, SendMail, SmartReply, SmartForward, MoveItems, MoveViaItemOperations, ApplyEmailChanges, SearchEmail, SearchEmailQuery, FindEmail, EmptyFolderContents
Calendar SyncCalendar, CreateEvent, UpdateEvent, DeleteEvent, RespondInvite (+ types Recurrence, Exception, EASTimeZone, EncodeTimeZone)
Contacts / Tasks / Notes SyncContacts, SyncTasks, SyncNotes + {Create,Update,Delete}{Contact,Task,Note} + CompleteTask + GALSearch
Settings SettingsDeviceInformation, GetOof, SetOof, GetUserInformation, GetRightsManagementTemplates, SetDevicePassword
Admin / protocol Options, NegotiateVersion, Provision, AcknowledgeRemoteWipe, Ping, ResolveRecipients, ValidateCert, Autodiscover
Document Library FetchDocumentLibrary (legacy SharePoint-over-EAS)

Per-symbol API docs live in godoc.

Authentication

Auth scheme decision tree

Scheme What to set When to use
Basic Config.Username + Config.Password Default; Z-Push and SOGo
Bearer (OAuth) Config.AuthHeader = func(ctx) (string, error) { … } + Config.RetryOn401 = true Office 365, any token-based identity provider
NTLM wrap cfg.HTTPClient.Transport with ntlmssp.Negotiator{}; username in DOMAIN\user form Legacy on-prem Exchange / IIS
Negotiate / SPNEGO wrap cfg.HTTPClient.Transport with a SPNEGO RoundTripper (e.g. via gokrb5/v8); supply keytab or use kinit ccache AD-joined environments
mTLS set tls.Config.Certificates on the underlying *http.Transport Server demands a client cert; combines with any of the above

AuthHeader is called per request, so it's the natural seam for OAuth token refresh. Pair with RetryOn401: true to get a transparent retry on token expiry.

Autodiscover

res, err := eas.Autodiscover(ctx, "user@example.com", password, eas.AutodiscoverOptions{})
// res.URL → Config.ServerURL

Autodiscover runs five candidate steps in order, matching Outlook's flow plus a well-known fallback:

  1. POST https://<domain>/Autodiscover/Autodiscover.xml
  2. POST https://autodiscover.<domain>/Autodiscover/Autodiscover.xml
  3. GET http://autodiscover.<domain>/... and follow a 302
  4. SRV record _autodiscover._tcp.<domain> → POST to the discovered host
  5. OPTIONS https://<domain|autodiscover.<domain>|mail.<domain>>/Microsoft-Server-ActiveSync

Step 5 is the well-known fallback. It handles deployments whose autodiscover responder does not speak the EAS mobilesync request schema (notably SOGo, which historically implements only the Outlook schema and rejects mobilesync with HTTP 400 <ErrorCode>601</ErrorCode>). When the schema-aware attempts all fail, the library probes the canonical EAS path with HTTP OPTIONS and accepts any 2xx response carrying an MS-Server-ActiveSync or MS-ASProtocolVersions header. The returned AutodiscoverResult has URL and ServerHostname set but no DisplayName (OPTIONS doesn't carry it).

Each step can be disabled via AutodiscoverOptions.Skip* flags. To pin a specific endpoint and skip discovery entirely, just set Config.ServerURL directly and don't call Autodiscover at all.

State persistence

type StateStore interface {
    PolicyKey(ctx context.Context) (string, error)
    SetPolicyKey(ctx context.Context, key string) error
    SyncKey(ctx context.Context, folderID string) (string, error)
    SetSyncKey(ctx context.Context, folderID, key string) error
}

Use NewMemoryState() for tests. For production, implement against whatever durable store you already have — bbolt, SQLite, Redis, your-favourite-K/V. Lose the SyncKey for a folder and you'll force a full resync of that folder; lose the PolicyKey and the next request will get a 449 and postRaw will re-Provision automatically.

Errors and transparent retries

if eas.IsHTTPStatus(err, 401) { … }    // 401, 403, etc.
if eas.IsStatusCode(err, 3) { … }      // EAS Status code (3 = InvalidSyncKey)
Error type Returned when
*HTTPError Server replied with non-2xx HTTP status. Includes StatusCode, Status, URL, first 4KiB of body.
*StatusError Server returned a parseable WBXML body with a non-1 Status element. Includes Command and EAS status code.

Two retries happen automatically inside Client:

  1. HTTP 401 + RetryOn401 + AuthHeader: refresh the bearer token and retry once. (OAuth flows where the token may have expired.)
  2. HTTP 449 (Retry With, Microsoft IIS extension): re-Provision and retry once. EAS servers expire policy keys aggressively; the spec mandates this recovery and we always do it.

SyncEmail, SyncCalendar, SyncContacts, SyncTasks, and SyncNotes also handle Status=3 InvalidSyncKey by clearing the local key and retrying once.

Calendar recurrence + timezones

loc, _ := time.LoadLocation("America/New_York")

draft := eas.EventDraft{
    Subject:   "Weekly review",
    StartTime: time.Date(2026, 6, 1, 14, 0, 0, 0, loc),
    EndTime:   time.Date(2026, 6, 1, 15, 0, 0, 0, loc),
    TimeZone:  &eas.EASTimeZone{}, // or build via helper for full DST fidelity
    Recurrence: &eas.Recurrence{
        Type:      eas.RecurrenceWeekly,
        Interval:  1,
        DayOfWeek: eas.DowMonday | eas.DowFriday,
        Until:     time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC),
    },
}
id, err := c.CreateEvent(ctx, calendarFolderID, draft)

Per-instance overrides go into draft.Exceptions. The EASTimeZone blob is the standard 172-byte Microsoft TIME_ZONE_INFORMATION struct; EncodeTimeZone(loc) produces a base64-ready value, and parsed events round-trip the original blob on EventItem.TimeZoneRaw.

Structured Search / Find queries

import "time"

q := eas.And(
    eas.EmailClass(),
    eas.EqualTo(eas.PropEmailFrom, "alice@example.com"),
    eas.GreaterThan(eas.PropEmailDateReceived,
        time.Now().Add(-30*24*time.Hour).Format("2006-01-02T15:04:05.000Z")),
)

res, err := c.SearchEmailQuery(ctx, q, eas.EmailSearchOptions{Range: "0-49"})

Use eas.SearchEmail for the simple free-text path, and SearchEmailQuery (or the 16.x-only FindEmail) when you need the structured operators.

S/MIME

signed, err := eas.SignMIME(plainMIME, eas.SMIMESigner{
    Certificate: signerCert,
    PrivateKey:  signerKey,
})

encrypted, err := eas.EncryptMIME(plainMIME, []*x509.Certificate{recipientCert})

// Combined: sign then encrypt, the order most MUAs expect.
out, err := eas.SignAndEncryptMIME(plainMIME,
    eas.SMIMESigner{Certificate: signerCert, PrivateKey: signerKey},
    []*x509.Certificate{recipientCert})

Recipient certificates can be retrieved via ResolveRecipients with CertificateRetrieval = 2 (Full).

Testing

Unit tests are pure-Go (go test ./eas). The integration suite (go test -tags integration ./eas) exercises every command against a live EAS server you point it at via env vars; see the testenv/ Docker stack for a one-command Z-Push setup, and the integration test file for runnable examples of every major command.

Testing your own code that depends on eas

eas.Client is an interface. The easmock subpackage provides hand-written test doubles — one struct per interface, with a *Func field per method. Configure only the fields your test cares about; unconfigured methods return a sentinel error so misbehaving code paths surface loudly.

import (
    "context"
    "testing"

    "github.com/hstern/go-activesync/eas"
    "github.com/hstern/go-activesync/eas/easmock"
)

func TestInboxSummary(t *testing.T) {
    var c eas.Client = &easmock.Client{
        EmailClient: easmock.EmailClient{
            SyncEmailFunc: func(_ context.Context, _ string, _ eas.EmailSyncOptions) (*eas.EmailSyncResult, error) {
                return &eas.EmailSyncResult{
                    Added: []eas.EmailItem{{Subject: "hi"}},
                }, nil
            },
        },
    }
    // ... pass c to your code under test ...
}

Sub-interfaces (EmailClient, CalendarClient, …) compose into the umbrella Client; consumers can depend on a slim view if they only touch one feature area.

Server-specific notes

Interop gaps caught by testing against live deployments. The library handles each one as gracefully as it can; the symptoms can be confusing without the context below.

Server Behavior What the library does
SOGo autodiscover The SOGo autodiscover module implements only the Outlook request schema, not EAS mobilesync. Returns HTTP 400 <ErrorCode>601</ErrorCode> "Not supported xmlns". Autodiscover falls through to the well-known fallback (step 5) and locates the EAS endpoint via an OPTIONS probe. No caller action needed.
Z-Push BackendIMAP ResolveRecipients BackendIMAP does not implement the GAL lookup hook, so every ResolveRecipients request comes back with EAS Status 5 (ServerError). The library surfaces a *StatusError with Command=ResolveRecipients Status=5. There is no library-side workaround for a server that doesn't implement the verb — catch and skip.
Z-Push BackendIMAP GetUserInformation Returns the primary email correctly but reports an empty Accounts list (no per-account detail). UserInformation{PrimaryEmail: …, Accounts: nil}. Treat the empty slice as expected on Z-Push.
Z-Push Ping <Folder> element shape Z-Push (correctly per MS-ASCMD §2.2.2.11.2) sends the changed folder ID as plain text content of <Folder>, not the nested <Folder><Id>…</Id></Folder> form seen elsewhere in EAS. The parser handles both shapes. Fixed in v0.2.

See also

Documentation

Overview

Package eas implements an Exchange ActiveSync (EAS) 14.1 client.

EAS is Microsoft's mobile-mail sync protocol, originally for Outlook Mobile but supported by open-source servers including Z-Push and SOGo. The wire format is HTTP(S) POST with WBXML bodies (see the wbxml package). All commands except Ping and OPTIONS use WBXML.

Usage

Client is an interface composed of one sub-interface per feature area (EmailClient, CalendarClient, ContactsClient, …). NewClient returns a Client; the concrete type is unexported. Provide pre-resolved credentials and a StateStore for SyncKey / PolicyKey persistence:

c, err := eas.NewClient(eas.Config{
    ServerURL: "https://mail.example.com/Microsoft-Server-ActiveSync",
    Username:  "henry",
    Password:  pw,                  // already retrieved from keyring
    DeviceID:  "32hexcharsofidhere",
    State:     eas.NewMemoryState(),
})
if err != nil { return err }

if err := c.Provision(ctx); err != nil { return err }
folders, err := c.FolderSync(ctx)

Callers that touch only a slice of the protocol can depend on the narrower sub-interface — e.g. an inbox-summarising tool can accept an EmailClient + FolderClient and stay decoupled from the rest.

Testing

The github.com/hstern/go-activesync/eas/easmock subpackage provides hand-written test doubles for Client and every sub-interface. Set the *Func fields you care about; methods whose Func is nil return a sentinel error so a test fails loudly rather than silently calling a no-op.

Stateful commands

Commands that change server-side state (Provision, Sync, FolderSync) read and write keys via the StateStore. Persist state across process restarts by providing a durable implementation; an in-memory store is supplied for tests and one-shot CLIs.

More

See README.md in this directory for an API tour with diagrams, per-class command tables, and worked examples (S/MIME, recurrence, structured search). Or browse the full godoc.

Index

Examples

Constants

View Source
const (
	StatusOK             int = 1
	StatusInvalidContent int = 101
	StatusInvalidWBXML   int = 102
	StatusServerError    int = 110
	// FolderSync / Sync
	StatusInvalidSyncKey int = 3
	// Provision
	StatusPolicyAcknowledged int = 1 // same numeric value as OK
)

EAS status codes that show up across the protocol-common path. Per-command codes (Provision, FolderSync, Sync) overlap heavily, and statusName returns the most generally accurate description.

MS-ASCMD §2.2.4 enumerates codes per command; we only name the ones we actually inspect or surface to users.

View Source
const FolderRootID = "__root__"

FolderRootID is the conventional folderID used to store the FolderSync SyncKey (which is per-account, not per-folder). Callers should not use this string as a real folder identifier.

Variables

View Source
var (
	PropEmailFrom         = SearchProp{Page: wbxml.PageEmail, Name: "From"}
	PropEmailTo           = SearchProp{Page: wbxml.PageEmail, Name: "To"}
	PropEmailSubject      = SearchProp{Page: wbxml.PageEmail, Name: "Subject"}
	PropEmailDateReceived = SearchProp{Page: wbxml.PageEmail, Name: "DateReceived"}
	PropEmailHasAttach    = SearchProp{Page: wbxml.PageAirSyncBase, Name: "Attachments"}
	PropConvoID           = SearchProp{Page: wbxml.PageEmail2, Name: "ConversationId"}
)

Pre-built property handles for common email comparisons.

Functions

func EncodeTimeZone

func EncodeTimeZone(loc *time.Location) string

EncodeTimeZone returns the base64-encoded text content for a Calendar TimeZone element representing loc. UTC produces an all-zero blob.

For non-UTC locations the encoding sets only the Bias field; DST rules are deliberately left empty because Go's *time.Location has no public API to extract them. Servers that need the full DST information should construct an EASTimeZone manually.

Example

EncodeTimeZone turns a Go *time.Location into the 172-byte Microsoft TIME_ZONE_INFORMATION blob that Calendar events use. Use the EASTimeZone struct directly for full DST-rule fidelity (Go's *time.Location doesn't expose the recurring rule).

package main

import (
	"fmt"
	"time"

	"github.com/hstern/go-activesync/eas"
)

func main() {
	utc := eas.EncodeTimeZone(time.UTC)
	fmt.Println("utc encoded length (base64):", len(utc))

	// Round-trip back.
	decoded, err := eas.DecodeTimeZone(utc)
	if err != nil {
		panic(err)
	}
	fmt.Println("bias minutes:", decoded.BiasMinutes)
}

func EncryptMIME

func EncryptMIME(mime []byte, recipientCerts []*x509.Certificate) ([]byte, error)

EncryptMIME wraps mime in an S/MIME enveloped-data structure (application/pkcs7-mime; smime-type=enveloped-data). recipientCerts must include each recipient's public certificate; obtain them via ResolveRecipients with CertificateRetrieval=2.

Default cipher: AES-128 CBC (the S/MIME 3 baseline). Callers needing AES-256 GCM should sign+encrypt manually with the pkcs7 library.

func IsHTTPStatus

func IsHTTPStatus(err error, code int) bool

IsHTTPStatus reports whether err is an HTTPError with the given code. Useful for callers that want to distinguish 401 / 403 / 449 etc. without type-asserting at every site.

func IsStatusCode

func IsStatusCode(err error, code int) bool

IsStatusCode reports whether err is a StatusError with the given EAS status code.

func RemoteWipeRequested

func RemoteWipeRequested(provisionResp *wbxml.Document) bool

RemoteWipeRequested reports whether the most recent Provision response contained a <RemoteWipe> element instructing the client to wipe its data. Callers should call AcknowledgeRemoteWipe to confirm the wipe has been (or will be) carried out, after which the server will refuse further commands until a new device id provisions cleanly.

Non-device callers (sync daemons, server-side agents) typically can't carry out a wipe in any literal sense; the conventional response is to delete any locally persisted state for the account before acknowledging.

func SignAndEncryptMIME

func SignAndEncryptMIME(mime []byte, signer SMIMESigner, recipientCerts []*x509.Certificate) ([]byte, error)

SignAndEncryptMIME signs mime then encrypts the signed envelope to recipientCerts. This is the form most MUAs expect for "signed and encrypted" delivery — the signature is verifiable only after decrypt.

func SignMIME

func SignMIME(mime []byte, signer SMIMESigner) ([]byte, error)

SignMIME wraps mime in a multipart/signed S/MIME envelope using PKCS#7 detached signatures (the "smime-type=signed-data" form most MUAs render correctly). The returned bytes are a complete RFC 5322 message body suitable for SendMail.

Example

SignMIME wraps a plain RFC 5322 message in an S/MIME multipart/signed envelope. EncryptMIME and SignAndEncryptMIME mirror this for recipient-side encryption.

package main

import (
	"crypto/x509"
	"fmt"

	"github.com/hstern/go-activesync/eas"
)

func main() {
	var (
		signerCert *x509.Certificate
		signerKey  any
	)
	plain := []byte("From: a@b\r\nTo: c@d\r\nSubject: hi\r\n\r\nhello")

	signed, err := eas.SignMIME(plain, eas.SMIMESigner{
		Certificate: signerCert,
		PrivateKey:  signerKey,
	})
	if err != nil {
		panic(err)
	}
	// `signed` is a complete multipart/signed message body, ready to
	// hand to Client.SendMail as SendMailOptions.MIME.
	fmt.Println(len(signed))
}

Types

type ApprovedApplication

type ApprovedApplication struct {
	Name string
	Hash string
}

ApprovedApplication is one entry in the policy's ApprovedApplicationList.

type AutodiscoverOptions

type AutodiscoverOptions struct {
	// HTTPClient transports the discovery requests. If nil, http.DefaultClient.
	HTTPClient *http.Client
	// Logger receives debug events. If nil, slog.Default().
	Logger *slog.Logger
	// UserAgent is sent in the request header. Default "go-activesync/0.1".
	UserAgent string
	// Endpoints overrides the default endpoint candidates (e.g. for tests).
	// When nil, autodiscoverEndpointsFor is used.
	Endpoints []string
	// SkipHTTPRedirect disables option 3 (the HTTP GET fallback that
	// reads a 302 from autodiscover.<domain>). Useful in environments
	// where unauthenticated HTTP probes might be blocked.
	SkipHTTPRedirect bool
	// SkipSRVLookup disables option 4 (DNS SRV record fallback). Useful
	// when DNS is restricted or the lookup is too slow.
	SkipSRVLookup bool
	// SkipWellKnownFallback disables option 5 (the well-known EAS path
	// probe). When every schema-aware Autodiscover attempt fails, the
	// library tries HTTP OPTIONS against the canonical EAS endpoint at
	// the bare domain, autodiscover.<domain>, and mail.<domain>. This
	// handles deployments whose autodiscover responder does not speak the
	// EAS mobilesync request schema (e.g. SOGo, which historically only
	// implements the Outlook schema).
	SkipWellKnownFallback bool
	// SRVLookup is the DNS SRV resolver. Defaults to net.DefaultResolver.
	// Tests inject a stub.
	SRVLookup func(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
}

AutodiscoverOptions configures a single Autodiscover query.

type AutodiscoverResult

type AutodiscoverResult struct {
	URL            string // the EAS endpoint URL the server reports
	ServerHostname string // the bare hostname (informational)
	DisplayName    string // the user's display name, if reported
}

AutodiscoverResult is the parsed output of a successful Autodiscover query for the "MobileSync" (i.e. EAS) protocol.

func Autodiscover

func Autodiscover(ctx context.Context, email, password string, opts AutodiscoverOptions) (*AutodiscoverResult, error)

Autodiscover queries the standard EAS Autodiscover endpoints for the given email address and returns the first successful response.

EAS Autodiscover ([MS-ASCMD] §2.2.2.1; the protocol predates XML namespacing conventions and uses raw element names) is an XML POST that returns the EAS endpoint URL. The full discovery flow tries four candidates in order, matching Outlook's behavior:

  1. POST https://<domain>/Autodiscover/Autodiscover.xml
  2. POST https://autodiscover.<domain>/Autodiscover/Autodiscover.xml
  3. GET http://autodiscover.<domain>/Autodiscover/Autodiscover.xml — expecting a 302 redirect to a https://… URL we then POST to.
  4. SRV _autodiscover._tcp.<domain> — query DNS for the host:port to POST to.
  5. OPTIONS https://<domain>/Microsoft-Server-ActiveSync (and the autodiscover.<domain> / mail.<domain> variants) — accept any 2xx response carrying an EAS server header. This is a last-resort fallback for deployments whose autodiscover service does not speak the mobilesync schema.

Each step can be disabled via AutodiscoverOptions. The password is required because Autodiscover responses are authenticated; this function does not store the password.

type BodyType

type BodyType int

BodyType maps the AirSyncBase body Type element values.

const (
	BodyTypeNone  BodyType = 0
	BodyTypePlain BodyType = 1
	BodyTypeHTML  BodyType = 2
	BodyTypeRTF   BodyType = 3
	BodyTypeMIME  BodyType = 4
)

type CalendarClient added in v1.0.0

type CalendarClient interface {
	SyncCalendar(ctx context.Context, folderID string, opts CalendarSyncOptions) (*CalendarSyncResult, error)
	CreateEvent(ctx context.Context, folderID string, draft EventDraft) (string, error)
	UpdateEvent(ctx context.Context, folderID, serverID string, draft EventDraft) error
	DeleteEvent(ctx context.Context, folderID, serverID string) error
	RespondInvite(ctx context.Context, folderID, serverID string, choice MeetingResponseChoice) (*MeetingResponseResult, error)
}

CalendarClient covers calendar sync, event CRUD, and meeting-response.

type CalendarSyncOptions

type CalendarSyncOptions struct {
	WindowSize  int
	DateFilter  FilterType
	NoBootstrap bool
}

CalendarSyncOptions controls a SyncCalendar request.

type CalendarSyncResult

type CalendarSyncResult struct {
	SyncKey       string
	MoreAvailable bool
	Added         []EventItem
	Changed       []EventItem
	Deleted       []string
}

CalendarSyncResult is the parsed output of a calendar Sync.

type CertValidation

type CertValidation struct {
	// Status is 1 (Success) when the certificate validates against the
	// server's trust chain. Other values per MS-ASCERT §2.2.4.74.
	Status int
}

CertValidation is the per-certificate result of ValidateCert.

type Client

type Client interface {
	EmailClient
	CalendarClient
	ContactsClient
	TasksClient
	NotesClient
	FolderClient
	SettingsClient
	SearchClient
	ProvisionClient
	PingClient

	// LastPolicy returns the most recently parsed Policy from a
	// Provision exchange on this client, or nil if no policy has been
	// received yet.
	LastPolicy() *Policy
}

Client is the full EAS client surface. It composes one interface per feature area so callers that only touch a slice of the protocol can depend on the narrower view (e.g. an inbox-summarising tool needs only EmailClient + FolderClient).

NewClient returns a value satisfying Client; the concrete type is unexported. For unit tests, the github.com/hstern/go-activesync/eas/easmock package provides hand-written test doubles for Client and each sub-interface.

func NewClient

func NewClient(cfg Config) (Client, error)

NewClient validates the Config, applies defaults, and returns a ready Client. The returned client makes no network calls until a command method is invoked.

The returned value satisfies the Client interface; the concrete type is unexported. For unit tests, see the easmock subpackage which provides hand-written test doubles.

Example

Minimal end-to-end usage: build a Client, provision once, list folders.

These examples are compiled by `go test` (and rendered on pkg.go.dev) but not executed — they touch the network. To run them for real, point at a live server with the EAS_INTEGRATION_* env vars and `go test -tags integration ./eas`.

package main

import (
	"context"
	"fmt"

	"github.com/hstern/go-activesync/eas"
)

func main() {
	c, err := eas.NewClient(eas.Config{
		ServerURL: "https://mail.example.com/Microsoft-Server-ActiveSync",
		Username:  "henry",
		Password:  "secret",
		DeviceID:  "32hexcharsofdeviceidhere00000000",
		State:     eas.NewMemoryState(),
	})
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	if _, err := c.NegotiateVersion(ctx); err != nil {
		panic(err)
	}
	if err := c.Provision(ctx); err != nil {
		panic(err)
	}

	folders, err := c.FolderSync(ctx)
	if err != nil {
		panic(err)
	}
	for _, f := range folders.Added {
		fmt.Printf("%s %s\n", f.Type, f.DisplayName)
	}
}

type Config

type Config struct {
	// ServerURL is the EAS endpoint, e.g. "https://mail/Microsoft-Server-ActiveSync".
	// Required.
	ServerURL string
	// Username + Password are sent as HTTP Basic auth. Both required.
	Username string
	Password string
	// DeviceID is a 32-hex-char client identifier. Required; create once
	// per account and persist (the server treats a new ID as a new device).
	DeviceID string
	// DeviceType is sent in the URL query and policy doc. Default
	// "GoActiveSync". Some servers log this string in admin tools;
	// callers usually want to override with their app's name.
	DeviceType string
	// ASVersion is the protocol version sent in MS-ASProtocolVersion.
	// Default "14.1".
	ASVersion string
	// UserAgent is the HTTP User-Agent. Default "go-activesync/0.1".
	UserAgent string
	// HTTPClient is the transport. Default http.DefaultClient.
	HTTPClient *http.Client
	// Logger receives debug events. Default slog.Default().
	Logger *slog.Logger
	// State is required for stateful commands (Provision, Sync, FolderSync).
	State StateStore
	// Registry is the WBXML codepage registry. Default wbxml.DefaultRegistry().
	Registry *wbxml.Registry
	// AuthHeader, when set, overrides Username+Password for the
	// Authorization header. Called per request so callers can refresh
	// short-lived tokens (e.g. OAuth bearer) lazily. Return the full
	// header value, e.g. "Bearer eyJhbGc...". When unset, the client
	// uses HTTP Basic with Username:Password.
	AuthHeader func(ctx context.Context) (string, error)
	// RetryOn401 enables one transparent retry on a 401 Unauthorized
	// response, after a fresh AuthHeader call. Useful for OAuth bearer
	// flows where the token may have expired between requests.
	RetryOn401 bool
	// GzipRequests, when true, compresses request bodies above
	// GzipMinBytes with gzip and sets Content-Encoding accordingly.
	// Most EAS servers (Z-Push 2.5+, Exchange 2010+) accept this.
	GzipRequests bool
	// GzipMinBytes is the minimum body size that triggers compression
	// when GzipRequests is true. Default 1 KiB.
	GzipMinBytes int
	// Base64URL, when true, packs Cmd/User/DeviceId/DeviceType/PolicyKey
	// into a single base64-encoded query parameter per MS-ASHTTP §2.2.1.1.2.
	// Smaller request URLs and slightly faster server-side parsing on
	// some Exchange deployments. Off by default since Z-Push and SOGo
	// are happier with plain query strings.
	Base64URL bool
}

Config holds the inputs needed to build a Client. Fields without defaults are marked "required"; the rest fall back to sane EAS 14.1 values.

type ContactAddress

type ContactAddress struct {
	Street     string
	City       string
	State      string
	PostalCode string
	Country    string
}

ContactAddress is a US-style address.

type ContactDraft

type ContactDraft = ContactItem

ContactDraft is the input to CreateContact / UpdateContact.

type ContactItem

type ContactItem struct {
	ServerID string

	FirstName  string
	LastName   string
	MiddleName string
	Title      string
	Suffix     string
	FileAs     string

	CompanyName    string
	Department     string
	JobTitle       string
	OfficeLocation string

	Email1Address string
	Email2Address string
	Email3Address string

	HomePhone     string
	BusinessPhone string
	MobilePhone   string

	HomeAddress     ContactAddress
	BusinessAddress ContactAddress

	Birthday    time.Time
	Anniversary time.Time

	WebPage string

	// Picture is base64-encoded JPEG bytes of the contact photo. The
	// server typically caps these at 36 KB. On round-trip the bytes are
	// base64-encoded for transport but stored decoded here.
	Picture []byte

	// Categories is the user's freeform list of tags ("Work", "Family").
	Categories []string

	// Contacts2 (12.x) extras.
	NickName     string
	IMAddress    string
	IMAddress2   string
	IMAddress3   string
	ManagerName  string
	GovernmentID string
	CustomerID   string
}

ContactItem is a parsed contact from a Sync or Fetch response.

type ContactsClient added in v1.0.0

type ContactsClient interface {
	SyncContacts(ctx context.Context, folderID string) (*ContactsSyncResult, error)
	CreateContact(ctx context.Context, folderID string, draft ContactDraft) (string, error)
	UpdateContact(ctx context.Context, folderID, serverID string, draft ContactDraft) error
	DeleteContact(ctx context.Context, folderID, serverID string) error
}

ContactsClient covers contact-folder sync and CRUD.

type ContactsSyncResult

type ContactsSyncResult struct {
	SyncKey       string
	MoreAvailable bool
	Added         []ContactItem
	Changed       []ContactItem
	Deleted       []string
}

ContactsSyncResult is the parsed output of a contacts Sync.

type DayOfWeek

type DayOfWeek int

DayOfWeek is a bitmask used by Recurrence_DayOfWeek.

const (
	DowSunday    DayOfWeek = 1
	DowMonday    DayOfWeek = 2
	DowTuesday   DayOfWeek = 4
	DowWednesday DayOfWeek = 8
	DowThursday  DayOfWeek = 16
	DowFriday    DayOfWeek = 32
	DowSaturday  DayOfWeek = 64
	DowLastDay   DayOfWeek = 127 // for "last day of month" rules
)

type DeviceInformation

type DeviceInformation struct {
	Model          string // device model name (e.g. "iPhone15,3")
	IMEI           string // device IMEI; rarely meaningful for non-phone clients
	FriendlyName   string // human-friendly device name
	OS             string // device operating system (e.g. "darwin/amd64")
	OSLanguage     string // ISO language tag
	PhoneNumber    string
	MobileOperator string
	UserAgent      string
	// EnableOutboundSMS reports whether the device can send SMS via the
	// EAS server's SMS bridge. False for non-phone clients.
	EnableOutboundSMS bool
}

DeviceInformation is the payload for Settings/DeviceInformation/Set.

Per MS-ASPROV §3.1.5.1, in EAS 14.0+ the client SHOULD send this once (per device, per session) before issuing the initial Provision command. Strict Exchange servers will reject Provision otherwise; Z-Push and SOGo are lenient but accept the call as a no-op.

All fields default to empty strings if unset. The library sends what the caller provides; callers typically set Model + FriendlyName at minimum so the server can identify the client in its admin tools.

type EASTimeZone

type EASTimeZone struct {
	BiasMinutes  int32  // signed; the value Microsoft stores (UTC offset in negated minutes)
	StandardName string // up to 32 UTF-16 code units
	StandardDate SystemTime
	StandardBias int32
	DaylightName string
	DaylightDate SystemTime
	DaylightBias int32
}

EASTimeZone is a parsed/constructed TIME_ZONE_INFORMATION blob.

Most callers will use EncodeTimeZone(loc) for the common case (UTC or a Go *time.Location with simple DST rules). For full Outlook compatibility (named zone preserved across round-trip) construct the struct directly.

func DecodeTimeZone

func DecodeTimeZone(s string) (EASTimeZone, error)

DecodeTimeZone parses a base64-encoded TIME_ZONE_INFORMATION blob.

func (EASTimeZone) Encode

func (t EASTimeZone) Encode() string

Encode returns the base64-encoded text content for this EASTimeZone.

type EmailChange

type EmailChange struct {
	ServerID string
	// Read set to non-nil sets the Read flag. true=read, false=unread.
	Read *bool
	// Flagged set to non-nil sets the FlagStatus. true=2 (active),
	// false=0 (clear). Use SetFlagStatus for the full code.
	Flagged *bool
	// SetFlagStatus, when non-nil, sets the FlagStatus directly to the
	// EAS code (0=clear, 1=complete, 2=active). Overrides Flagged when
	// both are set.
	SetFlagStatus *int
	// Delete sends the change as a <Delete> rather than a <Change>.
	Delete bool
}

EmailChange describes one mutation to apply against a folder via the AirSync Sync command. Pointer fields are sentinel-nil for "leave unchanged" so a caller can target one attribute without disturbing the others.

type EmailChangeResult

type EmailChangeResult struct {
	ServerID string
	Status   int
}

EmailChangeResult is the per-change status reported by the server. Status 1 is success.

type EmailClient added in v1.0.0

type EmailClient interface {
	SyncEmail(ctx context.Context, folderID string, opts EmailSyncOptions) (*EmailSyncResult, error)
	ApplyEmailChanges(ctx context.Context, folderID string, changes []EmailChange) ([]EmailChangeResult, error)
	FetchEmail(ctx context.Context, folderID, serverID string, opts FetchEmailOptions) (*EmailItem, error)
	SendMail(ctx context.Context, opts SendMailOptions) error
	SmartReply(ctx context.Context, opts ReplyForwardOptions) error
	SmartForward(ctx context.Context, opts ReplyForwardOptions) error
	SearchEmail(ctx context.Context, query string, opts EmailSearchOptions) (*EmailSearchResult, error)
	SearchEmailQuery(ctx context.Context, q Query, opts EmailSearchOptions) (*EmailSearchResult, error)
	FindEmail(ctx context.Context, query string, opts FindOptions) (*FindResult, error)
}

EmailClient covers the email read/write/search/send surface.

type EmailItem

type EmailItem struct {
	ServerID string

	Subject      string
	From         string // raw "Name <addr>" form
	To           string // semicolon-separated, server's exact text
	Cc           string
	Bcc          string
	ReplyTo      string
	Sender       string
	DisplayTo    string
	DateReceived time.Time // zero if not parseable

	Read           bool
	FlagStatus     int // 0=clear, 1=complete, 2=active
	Importance     int // 0=low, 1=normal, 2=high
	HasAttachments bool
	ThreadTopic    string
	ConversationID []byte
	MessageClass   string

	BodyType          BodyType
	BodyEstimatedSize int
	BodyTruncated     bool
	Body              string // populated for BodyTypePlain / BodyTypeHTML / BodyTypeRTF
	BodyMIME          []byte // populated when BodyTypeMIME was requested
	BodyPreview       string // server-generated short preview, if any

	// Categories is the user's freeform tag list ("Work", "Family").
	Categories []string
	// VotingResponse is the recipient's vote on a voting-button mail
	// (Email2 codepage, 14.0+). Empty when the message has no buttons.
	VotingResponse string
	// VotingResponseOptions is the comma-separated list of available
	// vote choices the sender configured.
	VotingResponseOptions string
}

EmailItem is a parsed email returned by Sync or ItemOperations Fetch.

Fields are populated from whatever the server included; absent fields take their zero value. Servers vary; do not assume any single field is always present.

func (*EmailItem) Flagged

func (e *EmailItem) Flagged() bool

Flagged is a convenience for FlagStatus != 0.

type EmailSearchOptions

type EmailSearchOptions struct {
	// FolderID restricts the search to a single folder. Empty searches all
	// folders.
	FolderID string
	// DeepTraversal includes subfolders when FolderID is set.
	DeepTraversal bool
	// Range is the result window in EAS "<start>-<end>" form, inclusive
	// (e.g. "0-49" for the first 50 hits). Default "0-49".
	Range string
	// BodyPreviewBytes limits the per-hit body preview length. Default 256.
	BodyPreviewBytes int
	// RebuildResults forces the server to recompute its search index
	// rather than serving cached results. Off by default.
	RebuildResults bool
}

EmailSearchOptions controls a SearchEmail request.

type EmailSearchResult

type EmailSearchResult struct {
	// Items in result order. ServerID is set from the LongId element.
	Items []EmailItem
	// Range echoes the server's reported result window.
	Range string
	// Total is the server's estimate of the total matching items.
	Total int
}

EmailSearchResult is the parsed Search response.

type EmailSyncOptions

type EmailSyncOptions struct {
	WindowSize         int
	BodyType           BodyType
	BodyTruncationSize int
	DateFilter         FilterType
	NoBootstrap        bool
	// MIMESupport controls how the server returns MIME content:
	// 0=never (default), 1=for S/MIME only, 2=always.
	MIMESupport int
	// MIMETruncation, when >0, truncates the MIME blob to that many
	// bytes (different from BodyTruncationSize, which applies to the
	// AirSyncBase Body).
	MIMETruncation int
	// ConversationMode, when true, asks the server to deliver
	// conversation-grouped results (14.0+).
	ConversationMode bool
	// RightsManagementSupport, when true, includes IRM-protected items
	// in the response with the license metadata expanded (14.1+).
	RightsManagementSupport bool
	// BodyPart, when non-zero, requests a body-part preference
	// (a fragment of the body sized to the value, used for previews).
	BodyPartPreviewBytes int
	// Wait is the long-poll interval in minutes (1-59). When non-zero,
	// the server holds the request open until items are available or
	// Wait minutes elapse. Mutually exclusive with HeartbeatSeconds.
	WaitMinutes int
	// HeartbeatSeconds is the long-poll interval in seconds. EAS 14.0+
	// alternative to WaitMinutes with finer granularity. Mutually
	// exclusive with WaitMinutes.
	HeartbeatSeconds int
}

EmailSyncOptions controls a SyncEmail request.

Defaults applied when fields are zero:

  • WindowSize: 50
  • BodyType: BodyTypePlain
  • BodyTruncationSize: 32 KiB
  • DateFilter: FilterTwoWeek (a sensible default for "list my recent mail")

Set NoBootstrap=true to suppress the transparent two-call bootstrap when the persisted SyncKey is "0"; useful for tests that want to observe a single round-trip.

type EmailSyncResult

type EmailSyncResult struct {
	SyncKey       string
	MoreAvailable bool
	Added         []EmailItem
	Changed       []EmailItem
	Deleted       []string
}

EmailSyncResult is the parsed output of one SyncEmail round-trip (or the second of two round-trips when bootstrap was triggered).

type EventAttendee

type EventAttendee struct {
	Name           string
	Email          string
	AttendeeStatus int
	AttendeeType   int
}

EventAttendee describes one attendee on a calendar event.

type EventDraft

type EventDraft struct {
	Subject     string
	Location    string
	Body        string
	StartTime   time.Time
	EndTime     time.Time
	AllDayEvent bool
	BusyStatus  int // 0=free, 1=tentative, 2=busy, 3=OOF, 4=working elsewhere
	Sensitivity int // 0=normal, 1=personal, 2=private, 3=confidential
	Reminder    int // minutes before; 0 = none
	Attendees   []EventAttendee
	// Recurrence sets a repeat rule; nil means single-instance.
	Recurrence *Recurrence
	// Exceptions list per-instance overrides for a recurring event.
	Exceptions []Exception
	// TimeZone is the EAS time zone blob. EncodeTimeZone(loc) builds
	// one from a Go *time.Location for the simple case. When zero,
	// no TimeZone element is sent (the server uses its default).
	TimeZone *EASTimeZone
	// TimeZoneRaw is an alternative way to specify the time zone:
	// supply the base64 string the server gave you on a previous
	// fetch (for byte-identical round-trip).
	TimeZoneRaw string
}

EventDraft is the input to CreateEvent / UpdateEvent. All fields are optional except StartTime and EndTime; absent fields become "no change" on update or use sensible defaults on create.

type EventItem

type EventItem struct {
	ServerID string
	UID      string

	Subject       string
	Location      string
	Body          string
	BodyType      BodyType
	StartTime     time.Time
	EndTime       time.Time
	AllDayEvent   bool
	BusyStatus    int
	Sensitivity   int
	MeetingStatus int
	Reminder      int

	OrganizerName  string
	OrganizerEmail string
	Attendees      []EventAttendee

	// Recurrence is the repeat rule, when set.
	Recurrence *Recurrence
	// Exceptions are per-instance overrides.
	Exceptions []Exception
	// TimeZone is the parsed Microsoft TIME_ZONE_INFORMATION blob.
	// Zero value means the server didn't include one (Outlook fills
	// this in for recurring events; one-shot events often omit it).
	TimeZone EASTimeZone
	// TimeZoneRaw is the original base64 text from the server, useful
	// for byte-identical round-trip when updating the event.
	TimeZoneRaw string
}

EventItem is a parsed calendar event from a calendar Sync or ItemOperations Fetch response. Fields populated only when the server included them.

type Exception

type Exception struct {
	// ExceptionStartTime is the original instance time being overridden.
	ExceptionStartTime time.Time
	// Deleted, when true, removes the instance entirely; the other
	// fields are ignored.
	Deleted bool
	// Subject etc. — when non-empty, override the recurring event's
	// values for this instance only.
	Subject     string
	Location    string
	StartTime   time.Time
	EndTime     time.Time
	AllDayEvent bool
	BusyStatus  int
	Reminder    int
	Body        string
}

Exception is one date override on a recurring event.

type FetchAttachmentResult

type FetchAttachmentResult struct {
	// Data is the attachment payload (already gunzipped if Range was
	// not used).
	Data []byte
	// ContentType is the MIME type the server reported, when available.
	ContentType string
	// Range is the server-echoed byte range when partial fetch was
	// requested. Empty otherwise.
	Range string
}

FetchAttachmentResult holds the bytes and metadata of one attachment.

type FetchEmailOptions

type FetchEmailOptions struct {
	BodyType           BodyType // default BodyTypeMIME for full fidelity
	BodyTruncationSize int      // 0 = full body
}

FetchEmailOptions controls a FetchEmail request.

type FilterType

type FilterType int

FilterType is the EAS Sync FilterType element. It limits the date window of items returned in a Sync response.

const (
	FilterNone       FilterType = 0
	FilterOneDay     FilterType = 1
	FilterThreeDay   FilterType = 2
	FilterOneWeek    FilterType = 3
	FilterTwoWeek    FilterType = 4
	FilterOneMonth   FilterType = 5
	FilterThreeMonth FilterType = 6
	FilterSixMonth   FilterType = 7
)

Standard EAS FilterType values for email.

type FindHit

type FindHit struct {
	Item           EmailItem
	Preview        string
	HasAttachments bool
}

FindHit is one entry returned by Find. Properties are returned as raw EmailItem fields (subject, from, etc.) plus a few Find-specific fields like Preview.

type FindOptions

type FindOptions struct {
	// FolderID restricts the search to a single folder. Empty searches
	// the inbox by default per server policy.
	FolderID string
	// DeepTraversal includes subfolders when FolderID is set.
	DeepTraversal bool
	// Range is the result window in EAS "<start>-<end>" form, inclusive.
	// Default "0-49".
	Range string
	// PreviewBytes requests an HTML preview of each hit, up to N bytes.
	PreviewBytes int
}

FindOptions controls a Find request (EAS 16.0+).

type FindResult

type FindResult struct {
	Hits  []FindHit
	Range string
	Total int
}

FindResult is the parsed Find response.

type Folder

type Folder struct {
	ServerID    string
	ParentID    string
	DisplayName string
	Type        FolderType
}

Folder is one entry in the server's folder hierarchy.

type FolderClient added in v1.0.0

type FolderClient interface {
	FolderSync(ctx context.Context) (*FolderSyncResult, error)
	FolderCreate(ctx context.Context, parentID, displayName string, folderType FolderType) (*FolderCreateResult, error)
	FolderUpdate(ctx context.Context, serverID, newParentID, newDisplayName string) error
	FolderDelete(ctx context.Context, serverID string) error
	GetItemEstimate(ctx context.Context, folderIDs []string) ([]ItemEstimate, error)
	MoveItems(ctx context.Context, srcFolder, dstFolder string, ids []string) ([]MoveItemResult, error)
	MoveViaItemOperations(ctx context.Context, srcFolder, srcID, dstFolder string, moveAlways bool) (string, error)
	EmptyFolderContents(ctx context.Context, folderID string, deleteSubfolders bool) error
	FetchAttachment(ctx context.Context, fileReference string, rangeStart, rangeEnd int64) (*FetchAttachmentResult, error)
	FetchDocumentLibrary(ctx context.Context, linkID string, rangeStart, rangeEnd int64) ([]byte, error)
}

FolderClient covers folder-hierarchy sync, folder CRUD, item-estimate, move-items, attachment / document-library fetch, and folder-empty.

type FolderCreateResult

type FolderCreateResult struct {
	// ServerID is the new folder's server-assigned identifier.
	ServerID string
	// SyncKey is the new FolderSync key the server returns. Already
	// persisted in the StateStore by the helper.
	SyncKey string
	// Status is 1 on success.
	Status int
}

FolderCreateResult is the parsed result of FolderCreate.

type FolderSyncResult

type FolderSyncResult struct {
	// SyncKey is the new key the server returned. Already persisted in the
	// StateStore by FolderSync; surfaced here for diagnostics.
	SyncKey string
	// Added are folders the server reports as new since the prior SyncKey.
	// On the first call (SyncKey "0") this is the entire folder hierarchy.
	Added []Folder
	// Updated are folders whose metadata changed (typically a rename or
	// reparent).
	Updated []Folder
	// Deleted are server IDs of folders the user removed.
	Deleted []string
}

FolderSyncResult is the parsed FolderSync response.

type FolderType

type FolderType int

FolderType is the EAS class of a folder. Values match MS-ASCMD §2.2.3.171.

const (
	FolderTypeUserGeneric    FolderType = 1
	FolderTypeInbox          FolderType = 2
	FolderTypeDrafts         FolderType = 3
	FolderTypeDeletedItems   FolderType = 4
	FolderTypeSentItems      FolderType = 5
	FolderTypeOutbox         FolderType = 6
	FolderTypeTasks          FolderType = 7
	FolderTypeCalendar       FolderType = 8
	FolderTypeContacts       FolderType = 9
	FolderTypeNotes          FolderType = 10
	FolderTypeJournal        FolderType = 11
	FolderTypeUserMail       FolderType = 12
	FolderTypeUserCalendar   FolderType = 13
	FolderTypeUserContacts   FolderType = 14
	FolderTypeUserTasks      FolderType = 15
	FolderTypeUserJournal    FolderType = 16
	FolderTypeUserNotes      FolderType = 17
	FolderTypeUnknown        FolderType = 18
	FolderTypeRecipientCache FolderType = 19
)

FolderType values used by EAS 14.1.

func (FolderType) String

func (t FolderType) String() string

String returns a short label for diagnostics.

type GALEntry

type GALEntry struct {
	DisplayName  string
	FirstName    string
	LastName     string
	Title        string
	Office       string
	Company      string
	Alias        string
	EmailAddress string
	Phone        string
	HomePhone    string
	MobilePhone  string
}

GALEntry is a Global Address List directory entry.

type GALSearchResult

type GALSearchResult struct {
	Entries []GALEntry
	Range   string
	Total   int
}

GALSearchResult is the parsed GAL search response.

type HTTPError

type HTTPError struct {
	StatusCode int
	Status     string
	URL        string
	Body       []byte // truncated to 4 KiB
}

HTTPError is returned when an EAS request receives a non-2xx HTTP response from the server. The body is captured (truncated to 4 KiB) so callers can include it in diagnostics without re-issuing the request.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type ItemEstimate

type ItemEstimate struct {
	CollectionID string
	Class        string
	Estimate     int
	Status       int
}

ItemEstimate is the per-collection result of GetItemEstimate.

type MeetingResponseChoice

type MeetingResponseChoice int

MeetingResponseChoice is a UserResponse value per MS-ASCMD §2.2.3.196.

const (
	MeetingAccept    MeetingResponseChoice = 1
	MeetingTentative MeetingResponseChoice = 2
	MeetingDecline   MeetingResponseChoice = 3
)

type MeetingResponseResult

type MeetingResponseResult struct {
	CalendarID string
	Status     int
}

MeetingResponseResult reports a CalendarId assignment if the response produced a calendar event (typical for accepts).

type MemoryState

type MemoryState struct {
	// contains filtered or unexported fields
}

MemoryState is an in-memory StateStore. Useful for tests and one-shot CLI tools where state need not survive process exit. Safe for concurrent use.

func NewMemoryState

func NewMemoryState() *MemoryState

NewMemoryState returns an empty in-memory StateStore.

func (*MemoryState) PolicyKey

func (m *MemoryState) PolicyKey(_ context.Context) (string, error)

PolicyKey implements StateStore.

func (*MemoryState) SetPolicyKey

func (m *MemoryState) SetPolicyKey(_ context.Context, key string) error

SetPolicyKey implements StateStore.

func (*MemoryState) SetSyncKey

func (m *MemoryState) SetSyncKey(_ context.Context, folderID, key string) error

SetSyncKey implements StateStore.

func (*MemoryState) SyncKey

func (m *MemoryState) SyncKey(_ context.Context, folderID string) (string, error)

SyncKey implements StateStore.

type MoveItemResult

type MoveItemResult struct {
	SrcServerID string
	DstServerID string // assigned by destination folder
	Status      int
}

MoveItemResult is the per-item outcome of MoveItems.

type NoteDraft

type NoteDraft = NoteItem

NoteDraft is the input to CreateNote / UpdateNote.

type NoteItem

type NoteItem struct {
	ServerID         string
	Subject          string
	Body             string
	BodyType         BodyType // typically Plain or HTML
	LastModifiedDate time.Time
	Categories       []string
}

NoteItem is a parsed note item.

type NotesClient added in v1.0.0

type NotesClient interface {
	SyncNotes(ctx context.Context, folderID string) (*NotesSyncResult, error)
	CreateNote(ctx context.Context, folderID string, draft NoteDraft) (string, error)
	UpdateNote(ctx context.Context, folderID, serverID string, draft NoteDraft) error
	DeleteNote(ctx context.Context, folderID, serverID string) error
}

NotesClient covers notes-folder sync and CRUD.

type NotesSyncResult

type NotesSyncResult struct {
	SyncKey       string
	MoreAvailable bool
	Added         []NoteItem
	Changed       []NoteItem
	Deleted       []string
}

NotesSyncResult is the parsed Sync output for a notes folder.

type OofConfig

type OofConfig struct {
	State                OofState
	StartTime            time.Time
	EndTime              time.Time
	InternalReply        OofMessage
	ExternalKnownReply   OofMessage
	ExternalUnknownReply OofMessage
}

OofConfig is the user's full Out-of-Office configuration.

type OofMessage

type OofMessage struct {
	Enabled      bool
	ReplyMessage string
	BodyType     BodyType // BodyTypePlain or BodyTypeHTML
}

OofMessage is one of the three OOF reply variants (internal, external-known, external-unknown).

type OofState

type OofState int

OofState matches MS-ASCMD §2.2.3.139 (OofState).

const (
	OofDisabled  OofState = 0
	OofGlobal    OofState = 1
	OofTimeBased OofState = 2
)

type OptionsResult

type OptionsResult struct {
	// ProtocolVersions are the EAS protocol versions the server supports
	// (e.g. ["2.5", "12.0", "12.1", "14.0", "14.1"]).
	ProtocolVersions []string
	// Commands are the EAS commands the server accepts (e.g. ["Sync",
	// "FolderSync", "Provision", ...]).
	Commands []string
}

OptionsResult reports what a server says it supports in response to an HTTP OPTIONS request. EAS servers advertise this via two non-standard response headers; both are comma-separated.

func (*OptionsResult) HasCommand

func (o *OptionsResult) HasCommand(name string) bool

HasCommand reports whether the server claims to support the given command.

func (*OptionsResult) Supports

func (o *OptionsResult) Supports(version string) bool

Supports reports whether the server claims to support the given protocol version (e.g. "14.1").

type PingClient added in v1.0.0

type PingClient interface {
	Ping(ctx context.Context, heartbeatSeconds int, folders []PingFolder) (*PingResult, error)
}

PingClient covers long-poll change notification.

type PingFolder

type PingFolder struct {
	ID    string
	Class string
}

PingFolder describes one folder + class to subscribe to in a Ping request. Class is "Email", "Calendar", "Contacts", "Tasks", or "Notes".

type PingResult

type PingResult struct {
	// Status is the EAS Ping status code (1=OK no changes, 2=changes
	// available, 3=heartbeat too short, 4=heartbeat too long, 5=too many
	// folders, 6=missing parameters, 7=folder hierarchy out of date,
	// 8=server error).
	Status int
	// ChangedFolders lists the folder IDs the server reports have
	// pending changes; populated when Status=2.
	ChangedFolders []string
	// HeartbeatInterval is the server-recommended heartbeat in seconds;
	// non-zero on Status 3 or 4 to indicate the acceptable range.
	HeartbeatInterval int
}

PingResult is the parsed Ping response.

type Policy

type Policy struct {
	// Password / lock policy.
	DevicePasswordEnabled              bool
	AlphanumericDevicePasswordRequired bool
	AllowSimpleDevicePassword          bool
	MinDevicePasswordLength            int
	MinDevicePasswordComplexCharacters int
	MaxDevicePasswordFailedAttempts    int
	MaxInactivityTimeDeviceLockSeconds int
	DevicePasswordExpirationDays       int
	DevicePasswordHistory              int
	RequireStorageCardEncryption       bool
	RequireDeviceEncryption            bool

	// Hardware feature toggles.
	AllowCamera           bool
	AllowStorageCard      bool
	AllowWiFi             bool
	AllowTextMessaging    bool
	AllowBluetooth        int // 0 disabled, 1 handsfree-only, 2 allowed
	AllowIrDA             bool
	AllowInternetSharing  bool
	AllowRemoteDesktop    bool
	AllowDesktopSync      bool
	AllowBrowser          bool
	AllowConsumerEmail    bool
	AllowPOPIMAPEmail     bool
	AllowUnsignedApps     bool
	AllowUnsignedInstall  bool
	RequireManualSyncRoam bool

	// Mail / calendar limits.
	AllowHTMLEmail                 bool
	MaxAttachmentSizeBytes         int
	MaxCalendarAgeFilter           int // EAS filter type
	MaxEmailAgeFilter              int
	MaxEmailBodyTruncationSize     int
	MaxEmailHTMLBodyTruncationSize int
	AttachmentsEnabled             bool

	// S/MIME.
	RequireSignedSMIMEMessages               bool
	RequireEncryptedSMIMEMessages            bool
	RequireSignedSMIMEAlgorithm              int
	RequireEncryptionSMIMEAlgorithm          int
	AllowSMIMEEncryptionAlgorithmNegotiation int
	AllowSMIMESoftCerts                      bool

	// Application allow/deny lists (parsed but rarely meaningful for us).
	UnapprovedInROMApplicationList []string
	ApprovedApplicationList        []ApprovedApplication

	// Hash is the policy revision hash; stable until the server changes
	// any field above. Useful for caching parsed policies across calls.
	Hash string
}

Policy is the parsed EASProvisionDoc the server returned during a Provision exchange. It captures every field defined by MS-ASPROV §2.2.2 for protocol versions 12.x–14.x.

Most fields describe constraints a real device would enforce (password rules, camera/storage/Bluetooth allowances, attachment limits). A library client can rarely enforce them directly; the Policy is exposed so callers can inspect what the server expects and refuse to operate when policy clashes with their security posture.

type ProvisionClient added in v1.0.0

type ProvisionClient interface {
	Provision(ctx context.Context) error
	AcknowledgeRemoteWipe(ctx context.Context, status int) error
	NegotiateVersion(ctx context.Context) (string, error)
	Options(ctx context.Context) (*OptionsResult, error)
}

ProvisionClient covers the policy handshake, remote-wipe ack, and pre-flight version negotiation / OPTIONS probing.

type Query

type Query interface {
	// contains filtered or unexported methods
}

Query is the AST for a structured EAS Search/Find query. Build trees from the constructors below; call EncodeQuery to turn them into the WBXML element placed inside <Query>.

The Search command's <Query> can hold a single child (FreeText, And, Or, EqualTo, etc.). Most callers want And + leaf comparisons.

func And

func And(qs ...Query) Query

And combines sub-queries with logical AND.

func CollectionID

func CollectionID(id string) Query

CollectionID restricts search to a specific folder. Convenience wrapper around an EqualTo against AirSync.CollectionId.

func EmailClass

func EmailClass() Query

EmailClass restricts the search to email items.

func EqualTo

func EqualTo(p SearchProp, value string) Query

EqualTo asserts that property equals value.

func FreeText

func FreeText(s string) Query

FreeText matches any item containing the given substring.

func GreaterThan

func GreaterThan(p SearchProp, value string) Query

GreaterThan asserts property > value.

func LessThan

func LessThan(p SearchProp, value string) Query

LessThan asserts property < value.

func Or

func Or(qs ...Query) Query

Or combines sub-queries with logical OR.

type Recurrence

type Recurrence struct {
	Type           RecurrenceType
	Interval       int       // every N days/weeks/months
	Until          time.Time // last instance; zero = forever
	Occurrences    int       // alternative to Until; 0 = unset
	DayOfWeek      DayOfWeek // for Weekly + Monthly/YearlyByDay
	DayOfMonth     int       // for MonthlyDate / YearlyDate
	WeekOfMonth    int       // 1-5 (5 = "last")
	MonthOfYear    int       // 1-12 for Yearly variants
	IsLeapMonth    bool      // 16.0+: lunar calendar quirk
	FirstDayOfWeek int       // 0=Sunday
	CalendarType   int       // 1=Gregorian etc.
}

Recurrence describes a calendar event recurrence pattern.

type RecurrenceType

type RecurrenceType int

RecurrenceType matches MS-ASCMD §2.2.3.149 (Type element under Recurrence).

const (
	RecurrenceDaily        RecurrenceType = 0
	RecurrenceWeekly       RecurrenceType = 1
	RecurrenceMonthlyDate  RecurrenceType = 2 // every Nth day-of-month
	RecurrenceMonthlyByDay RecurrenceType = 3 // 2nd Tuesday etc.
	RecurrenceYearlyDate   RecurrenceType = 5
	RecurrenceYearlyByDay  RecurrenceType = 6
)

type ReplyForwardOptions

type ReplyForwardOptions struct {
	SendMailOptions
	// FolderID and ServerID identify the source message being replied
	// to or forwarded.
	FolderID string
	ServerID string
}

ReplyForwardOptions controls a SmartReply or SmartForward call.

type ResolveOptions

type ResolveOptions struct {
	// CertificateRetrieval enables S/MIME cert lookup for each recipient.
	// 1=NoCertificate (default), 2=Full, 3=Mini.
	CertificateRetrieval int
	// MaxCertificates caps the cert count per recipient (default 99999).
	MaxCertificates int
	// MaxAmbiguousRecipients caps how many candidates an ambiguous name
	// returns. Default 100.
	MaxAmbiguousRecipients int
	// Availability requests free/busy data for each resolved recipient
	// in the given window. Both fields must be set to enable.
	AvailabilityStart time.Time
	AvailabilityEnd   time.Time
	// PictureMaxBytes requests a contact picture if non-zero.
	PictureMaxBytes int
}

ResolveOptions controls a ResolveRecipients request.

type ResolveResponse

type ResolveResponse struct {
	To         string
	Status     int
	Recipients []ResolvedRecipient
}

ResolveResponse is the per-input-recipient envelope.

type ResolvedRecipient

type ResolvedRecipient struct {
	// Type is 1=GAL, 2=ContactsFolder.
	Type         int
	DisplayName  string
	EmailAddress string
	// Certificates lists raw S/MIME certificate bytes (one per cert).
	Certificates [][]byte
	// MergedFreeBusy is the EAS free/busy string when Availability was
	// requested: each character is a 30-minute slot, "0"=free, "1"=tentative,
	// "2"=busy, "3"=OOF, "4"=workingElsewhere.
	MergedFreeBusy string
	// Picture, if requested and present.
	Picture []byte
}

ResolvedRecipient is one entry in the ResolveRecipients response.

type RightsLicense

type RightsLicense struct {
	Owner                            string
	ContentOwner                     string
	ContentExpiryDate                string // ISO 8601
	TemplateID                       string
	TemplateName                     string
	EditAllowed                      bool
	ReplyAllowed                     bool
	ReplyAllAllowed                  bool
	ForwardAllowed                   bool
	ModifyRecipientsAllowed          bool
	ExtractAllowed                   bool
	PrintAllowed                     bool
	ExportAllowed                    bool
	ProgrammaticAccessAllowed        bool
	RemoveRightsManagementProtection bool
}

RightsLicense describes the IRM-imposed constraints on an item, as reported in the AirSyncBase Body / RightsManagementLicense element when an item is fetched with RightsManagementSupport=1.

func ParseRightsLicense

func ParseRightsLicense(license *wbxml.Element) RightsLicense

ParseRightsLicense extracts a RightsLicense from a <RightsManagementLicense> element (typically nested inside an item's Body). Exposed as a helper so callers that fetch IRM-protected items via Sync or ItemOperations can interpret the license metadata.

type RightsTemplate

type RightsTemplate struct {
	TemplateID  string
	Name        string
	Description string
}

RightsTemplate is one IRM (Information Rights Management) template the server reports as available for new outbound messages.

type SMIMESigner

type SMIMESigner struct {
	Certificate *x509.Certificate
	PrivateKey  crypto.PrivateKey
	// Intermediates are CAs to include in the SignedData chain so the
	// recipient can verify without consulting AIA URLs.
	Intermediates []*x509.Certificate
}

SMIMESigner identifies the certificate + private key used to sign outbound mail. Pass to SignMIME or to SendMailOptions.SMIMESign.

type SearchClient added in v1.0.0

type SearchClient interface {
	GALSearch(ctx context.Context, query string, limit int) (*GALSearchResult, error)
	ResolveRecipients(ctx context.Context, recipients []string, opts ResolveOptions) ([]ResolveResponse, error)
	ValidateCert(ctx context.Context, certs, chain [][]byte, checkCRL bool) ([]CertValidation, error)
}

SearchClient covers GAL search, recipient resolution, and S/MIME certificate validation.

type SearchProp

type SearchProp struct {
	Page byte
	Name string
}

SearchProp identifies a property by codepage + element name. Helpers below provide pre-built ones for common email/contact fields.

type SendMailOptions

type SendMailOptions struct {
	// MIME is the full RFC 5322 message bytes. Required.
	MIME []byte
	// SaveInSent toggles whether the server stores a copy in Sent Items.
	// Default true.
	SaveInSent bool
	// ClientID is an opaque idempotency key. If empty, a random 32-hex
	// value is generated; the same ClientID may be retried safely if a
	// previous attempt's status is unknown.
	ClientID string
	// SkipSaveInSent inverts SaveInSent without forcing the caller to
	// pass false explicitly (Go zero-value is false, which we want to
	// mean "save in sent" by default).
	SkipSaveInSent bool
}

SendMailOptions controls a SendMail / SmartReply / SmartForward call.

type SettingsClient added in v1.0.0

type SettingsClient interface {
	GetOof(ctx context.Context) (*OofConfig, error)
	SetOof(ctx context.Context, cfg OofConfig) error
	SetDevicePassword(ctx context.Context, newPassword string) error
	GetUserInformation(ctx context.Context) (*UserInformation, error)
	SettingsDeviceInformation(ctx context.Context, info DeviceInformation) error
	GetRightsManagementTemplates(ctx context.Context) ([]RightsTemplate, error)
}

SettingsClient covers Out-of-Office, device password, user info, and rights-management template enumeration.

type StateStore

type StateStore interface {
	// PolicyKey returns the persisted policy key, or an empty string if the
	// account has not been provisioned yet. A nil error with an empty string
	// is the expected pre-provisioning state.
	PolicyKey(ctx context.Context) (string, error)
	// SetPolicyKey persists the provided key. An empty string clears the
	// stored key (used when re-provisioning is forced by a 449 response).
	SetPolicyKey(ctx context.Context, key string) error

	// SyncKey returns the most recently acknowledged SyncKey for folderID,
	// or "0" if no Sync has succeeded for that folder yet.
	SyncKey(ctx context.Context, folderID string) (string, error)
	// SetSyncKey persists the SyncKey returned by the server.
	SetSyncKey(ctx context.Context, folderID, key string) error
}

StateStore persists the per-account state that EAS protocol exchanges require: the policy key from the most recent Provision and the per-folder SyncKey for incremental Sync.

All methods take a context for cancellation; implementations backed by remote storage should honor it.

FolderID is the EAS server-assigned folder identifier (an opaque string; for the special root FolderSync state, callers use FolderRootID).

type StatusError

type StatusError struct {
	Command string // EAS command name ("FolderSync", "Provision", ...)
	Code    int
}

StatusError is returned when an EAS command parses successfully but the embedded Status element reports a non-success code. The mapping of codes to human-readable names lives in status.go.

func (*StatusError) Error

func (e *StatusError) Error() string

type SystemTime

type SystemTime struct {
	Year         uint16
	Month        uint16
	DayOfWeek    uint16
	Day          uint16
	Hour         uint16
	Minute       uint16
	Second       uint16
	Milliseconds uint16
}

SystemTime is the Windows SYSTEMTIME struct.

type TaskDraft

type TaskDraft = TaskItem

TaskDraft is the input to CreateTask / UpdateTask.

type TaskItem

type TaskItem struct {
	ServerID string

	Subject       string
	Body          string
	Importance    int // 0=low, 1=normal, 2=high
	Sensitivity   int
	Complete      bool
	DateCompleted time.Time
	StartDate     time.Time
	UTCStartDate  time.Time
	DueDate       time.Time
	UTCDueDate    time.Time
	Reminder      time.Time
}

TaskItem is a parsed task item.

type TasksClient added in v1.0.0

type TasksClient interface {
	SyncTasks(ctx context.Context, folderID string) (*TasksSyncResult, error)
	CreateTask(ctx context.Context, folderID string, draft TaskDraft) (string, error)
	UpdateTask(ctx context.Context, folderID, serverID string, draft TaskDraft) error
	CompleteTask(ctx context.Context, folderID, serverID string) error
	DeleteTask(ctx context.Context, folderID, serverID string) error
}

TasksClient covers task-folder sync, CRUD, and the convenience CompleteTask helper.

type TasksSyncResult

type TasksSyncResult struct {
	SyncKey       string
	MoreAvailable bool
	Added         []TaskItem
	Changed       []TaskItem
	Deleted       []string
}

TasksSyncResult is the parsed Sync output for a tasks folder.

type UserAccount

type UserAccount struct {
	AccountID       string
	AccountName     string
	UserDisplayName string
	PrimarySMTP     string
	SendDisabled    bool
}

UserAccount describes one of the user's mail accounts as known to the EAS server (Exchange typically reports just the primary).

type UserInformation

type UserInformation struct {
	PrimaryEmail string
	Accounts     []UserAccount
}

UserInformation is the result of Settings/UserInformation/Get.

Directories

Path Synopsis
Package easmock provides hand-written test doubles for the github.com/hstern/go-activesync/eas interface set.
Package easmock provides hand-written test doubles for the github.com/hstern/go-activesync/eas interface set.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL