### Install and Build hydroxide Source: https://github.com/emersion/hydroxide/blob/master/README.md Clone the repository and build the hydroxide executable. This is the initial setup step for running hydroxide. ```shell git clone https://github.com/emersion/hydroxide.git go build ./cmd/hydroxide ``` -------------------------------- ### Run hydroxide as an SMTP Server Source: https://github.com/emersion/hydroxide/blob/master/README.md Start hydroxide to function as an SMTP server. Configure your email client with the provided settings, using the generated bridge password. ```shell hydroxide smtp ``` -------------------------------- ### Run All Servers Concurrently with hydroxide Source: https://context7.com/emersion/hydroxide/llms.txt Starts SMTP, IMAP, and CardDAV servers in a single process for production use. Supports custom ports and TLS for all services. ```sh ./hydroxide serve ``` ```sh # With custom ports and TLS for all services: ./hydroxide \ --smtp-host 127.0.0.1 --smtp-port 1025 \ --imap-host 127.0.0.1 --imap-port 1143 \ --carddav-host 0.0.0.0 --carddav-port 8080 \ --carddav-tls-cert /etc/ssl/cert.pem \ --carddav-tls-key /etc/ssl/key.pem \ serve ``` ```systemd [Service] ExecStart=/usr/local/bin/hydroxide serve Environment="HYDROXIDE_BRIDGE_PASS=dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ==" Restart=on-failure ``` -------------------------------- ### Run CardDAV Server with hydroxide Source: https://context7.com/emersion/hydroxide/llms.txt Starts a CardDAV HTTP server on port 8080. Requires an HTTPS reverse proxy. Contacts are served as vCards. ```sh # Start CardDAV server (requires HTTPS reverse proxy in front) ./hydroxide carddav ``` ```nginx server { listen 443 ssl; server_name carddav.example.com; ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem; location / { proxy_pass http://127.0.0.1:8080; } } ``` ```sh # Manual curl test (list contacts): curl -u "alice@protonmail.com:bridgepassword" \ -X PROPFIND \ -H "Depth: 1" \ http://localhost:8080/contacts/default/ ``` -------------------------------- ### Run hydroxide as an IMAP Server Source: https://github.com/emersion/hydroxide/blob/master/README.md Start hydroxide to provide IMAP access. Note that IMAP support is currently a work-in-progress and only supports unencrypted local connections. ```shell hydroxide imap ``` -------------------------------- ### Run IMAP Server with hydroxide Source: https://context7.com/emersion/hydroxide/llms.txt Starts a local IMAP server on port 1143. Supports TLS configuration. IMAP mailboxes are mapped to ProtonMail labels. ```sh # Start IMAP server ./hydroxide imap ``` ```sh # With TLS: ./hydroxide --imap-tls-cert cert.pem --imap-tls-key key.pem imap ``` -------------------------------- ### Run hydroxide SMTP Server Source: https://context7.com/emersion/hydroxide/llms.txt Starts a local SMTP server for sending encrypted emails via the ProtonMail API. Supports plain and TLS connections. ```sh # Start SMTP server (plain, no TLS — suitable for localhost only) ./hydroxide smtp # With TLS: ./hydroxide --smtp-tls-cert cert.pem --smtp-tls-key key.pem smtp # Configure your email client or git-send-email: # Host: localhost, Port: 1025, Auth: PLAIN # Username: alice@protonmail.com # Password: ``` -------------------------------- ### Resolve Configuration Path with config.Path Source: https://context7.com/emersion/hydroxide/llms.txt Use `config.Path` to get the absolute path to a file within the hydroxide configuration directory. The directory is created if it doesn't exist. This is useful for storing user-specific configuration files like credentials or databases. ```go package main import ( "log" "github.com/emersion/hydroxide/config" ) func main() { // Get path to the auth credentials file: authPath, err := config.Path("auth.json") if err != nil { log.Fatalf("config.Path: %v", err) } log.Printf("Auth file: %s", authPath) // Auth file: /home/alice/.config/hydroxide/auth.json // Get path to a per-user BoltDB database: dbPath, err := config.Path("alice@protonmail.com.db") if err != nil { log.Fatalf("config.Path: %v", err) } log.Printf("DB file: %s", dbPath) // DB file: /home/alice/.config/hydroxide/alice@protonmail.com.db } ``` -------------------------------- ### hydroxide smtp — Run SMTP Server Source: https://context7.com/emersion/hydroxide/llms.txt Starts a local SMTP server that listens on port 1025. It supports SASL PLAIN authentication using ProtonMail credentials and ensures all outgoing messages are fully encrypted before sending via the ProtonMail API. ```APIDOC ## `hydroxide smtp` — Run SMTP Server Starts a local SMTP server on port 1025. Clients authenticate with SASL PLAIN using the ProtonMail username and the bridge password. Outgoing messages are fully encrypted end-to-end before being sent via the ProtonMail API. ### Usage ```sh ./hydroxide smtp # With TLS support: ./hydroxide --smtp-tls-cert cert.pem --smtp-tls-key key.pem smtp ``` ### Configuration for Email Clients - **Host**: `localhost` - **Port**: `1025` - **Authentication**: `PLAIN` - **Username**: `` - **Password**: `` (obtained via `hydroxide auth`) ``` -------------------------------- ### Build hydroxide from Source Source: https://context7.com/emersion/hydroxide/llms.txt Clone the repository and compile the hydroxide binary using the Go toolchain. The binary will be available in the root directory after compilation. ```sh git clone https://github.com/emersion/hydroxide.git cd hydroxide go build ./cmd/hydroxide # Binary is now ./hydroxide ``` -------------------------------- ### Load TLS Configuration with config.TLS Source: https://context7.com/emersion/hydroxide/llms.txt Use `config.TLS` to load TLS certificates and keys for server configuration. It supports both one-way TLS (server certificate only) and mutual TLS (server certificate plus client CA certificate). ```go package main import ( "log" "github.com/emersion/hydroxide/config" ) func main() { // One-way TLS (server certificate only): tlsCfg, err := config.TLS("server.crt", "server.key", "") if err != nil { log.Fatalf("TLS: %v", err) } log.Printf("TLS config loaded, cipher suites: %d", len(tlsCfg.CipherSuites)) // Mutual TLS (server cert + client CA): mtlsCfg, err := config.TLS("server.crt", "server.key", "client-ca.crt") if err != nil { log.Fatalf("mTLS: %v", err) } _ = mtlsCfg // pass to listenAndServeSMTP / listenAndServeIMAP / listenAndServeCardDAV } ``` -------------------------------- ### config.TLS Source: https://context7.com/emersion/hydroxide/llms.txt Loads a TLS certificate and private key pair from disk and optionally configures mutual TLS by loading a client CA certificate pool. ```APIDOC ## `config.TLS` — Load TLS Configuration Loads a TLS certificate and private key pair from disk and optionally configures mutual TLS by loading a client CA certificate pool. Returns a `*tls.Config` suitable for passing directly to the SMTP, IMAP, or CardDAV server constructors. ### Parameters #### Path Parameters - **certFile** (string) - Required - The path to the server certificate file. - **keyFile** (string) - Required - The path to the server private key file. - **clientCACertFile** (string) - Optional - The path to the client CA certificate file for mutual TLS. ### Returns - **`*tls.Config`**: A TLS configuration object. - **error**: An error if the TLS configuration fails to load. ### Example ```go // One-way TLS (server certificate only): tlsCfg, err := config.TLS("server.crt", "server.key", "") if err != nil { log.Fatalf("TLS: %v", err) } // Mutual TLS (server cert + client CA): mtlsCfg, err := config.TLS("server.crt", "server.key", "client-ca.crt") if err != nil { log.Fatalf("mTLS: %v", err) } ``` ``` -------------------------------- ### Fetch and Log ProtonMail Labels and System IDs Source: https://context7.com/emersion/hydroxide/llms.txt Fetches all labels for a user and logs their details. It also demonstrates how to use predefined system label ID constants for direct mapping to IMAP mailboxes. Ensure an authenticated client is available before calling. ```go package main import ( "log" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client labels, err := c.ListLabels() if err != nil { log.Fatalf("ListLabels: %v", err) } for _, l := range labels { log.Printf("Label: %s (ID: %s, Type: %d, Exclusive: %d)", l.Name, l.ID, l.Type, l.Exclusive) } // System label ID constants for direct use: log.Printf("Inbox ID: %s", protonmail.LabelInbox) // "0" log.Printf("Drafts ID: %s", protonmail.LabelDraft) // "8" log.Printf("Sent ID: %s", protonmail.LabelSent) // "7" log.Printf("Trash ID: %s", protonmail.LabelTrash) // "3" log.Printf("Spam ID: %s", protonmail.LabelSpam) // "4" log.Printf("Archive ID: %s", protonmail.LabelArchive) // "6" log.Printf("All Mail ID: %s", protonmail.LabelAllMail) // "5" log.Printf("Starred ID: %s", protonmail.LabelStarred) // "10" } ``` -------------------------------- ### Run hydroxide as a CardDAV Server Source: https://github.com/emersion/hydroxide/blob/master/README.md Launch hydroxide to serve CardDAV. It is recommended to set up an HTTPS reverse proxy for this mode. Tested with GNOME Evolution and Android DAVDroid. ```shell hydroxide carddav ``` -------------------------------- ### Initialize ProtonMail API Client Source: https://context7.com/emersion/hydroxide/llms.txt Initializes the core ProtonMail API client. Set `Debug` to true to log all HTTP request/response bodies. Automatically handles re-authentication on 401 errors. ```go package main import ( "log" "github.com/emersion/hydroxide/protonmail" ) func main() { c := &protonmail.Client{ RootURL: "https://mail.proton.me/api", AppVersion: "Other", Debug: true, // logs all HTTP request/response bodies to stderr } // After authentication (c.Unlock is called internally by the auth flow): user, err := c.GetCurrentUser() if err != nil { log.Fatalf("GetCurrentUser: %v", err) } log.Printf("User: %s, MaxSpace: %d bytes", user.Name, user.MaxSpace) addrs, err := c.ListAddresses() if err != nil { log.Fatalf("ListAddresses: %v", err) } for _, a := range addrs { log.Printf("Address: %s (send=%d receive=%d)", a.Email, a.Send, a.Receive) } } ``` -------------------------------- ### config.Path Source: https://context7.com/emersion/hydroxide/llms.txt Returns the absolute path to a named file inside the hydroxide configuration directory, creating the directory if it does not exist. ```APIDOC ## `config.Path` — Config Directory Resolution Returns the absolute path to a named file inside the hydroxide configuration directory (`~/.config/hydroxide/` on Linux, following XDG conventions), creating the directory if it does not exist. ### Parameters #### Path Parameters - **filename** (string) - Required - The name of the file to get the path for. ### Returns - **string**: The absolute path to the configuration file. - **error**: An error if the directory cannot be created or the path cannot be determined. ### Example ```go authPath, err := config.Path("auth.json") if err != nil { log.Fatalf("config.Path: %v", err) } log.Printf("Auth file: %s", authPath) ``` ``` -------------------------------- ### List and Create ProtonMail Contacts Source: https://context7.com/emersion/hydroxide/llms.txt Lists contacts with pagination and creates or updates contacts. Contact cards are split into cleartext-signed and encrypted components. ```go package main import ( "log" "github.com/emersion/go-vcard" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client // List first 100 contacts total, contacts, err := c.ListContacts(0, 100) if err != nil { log.Fatalf("ListContacts: %v", err) } log.Printf("Total contacts: %d, fetched: %d", total, len(contacts)) for _, contact := range contacts { full, err := c.GetContact(contact.ID) if err != nil { continue } for _, card := range full.Cards { plain, err := card.Read(c.KeyRing()) if err != nil { continue } log.Printf("Contact: %s", plain.Get(vcard.FieldFormattedName)) } } // Create a new contact card := vcard.Card{} card.SetValue(vcard.FieldFormattedName, "Bob Smith") card.SetValue(vcard.FieldUID, "bob-uid-001") vcard.AddEmailField(card, &vcard.Field{Value: "bob@example.com"}) encCard, err := protonmail.NewEncryptedContactCard(card, c.KeyRing(), c.KeyRing()) if err != nil { log.Fatalf("NewEncryptedContactCard: %v", err) } signedCard, err := protonmail.NewSignedContactCard(card, c.KeyRing()) if err != nil { log.Fatalf("NewSignedContactCard: %v", err) } created, err := c.CreateContacts([]*protonmail.Contact{ {Cards: []*protonmail.ContactCard{encCard, signedCard}}, }) if err != nil { log.Fatalf("CreateContacts: %v", err) } log.Printf("Created contact ID: %s", created[0].ID) } ``` -------------------------------- ### List and Fetch Messages Source: https://context7.com/emersion/hydroxide/llms.txt Lists messages with optional filters and fetches full message metadata. Use `Message.Read()` to decrypt the message body using the unlocked key ring. ```go package main import ( "log" "github.com/emersion/hydroxide/protonmail" ) func main() { // c is an authenticated *protonmail.Client with key ring unlocked var c *protonmail.Client filter := &protonmail.MessageFilter{ Page: 0, PageSize: 25, Label: protonmail.LabelInbox, // "0" } total, msgs, err := c.ListMessages(filter) if err != nil { log.Fatalf("ListMessages: %v", err) } log.Printf("Inbox: %d total, fetched %d", total, len(msgs)) for _, m := range msgs { full, err := c.GetMessage(m.ID) if err != nil { log.Printf("GetMessage %s: %v", m.ID, err) continue } // Decrypt the message body using the unlocked key ring body, err := full.Read(c.KeyRing()) if err != nil { log.Printf("decrypt message %s: %v", m.ID, err) continue } log.Printf("Subject: %s\nBody snippet: %.100s\n", full.Subject, body) } } ``` -------------------------------- ### Authenticate with ProtonMail and Store Credentials Source: https://context7.com/emersion/hydroxide/llms.txt Use the `hydroxide auth` command to perform SRP-based login, store encrypted credentials, and obtain a bridge password for email clients. The bridge password is not stored and must be provided separately. ```sh # Log in with a ProtonMail username (prompts for ProtonMail password interactively) ./hydroxide auth alice@protonmail.com # Output: # Password: (hidden) # Bridge password: dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ== # # Keep the bridge password — it is not stored anywhere and is required for # configuring SMTP/IMAP/CardDAV clients. # To avoid interactive prompts, supply the bridge password via environment variable: export HYDROXIDE_BRIDGE_PASS="dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ==" # List all authenticated usernames stored in auth.json: ./hydroxide status ``` -------------------------------- ### protonmail.Client.ListContacts and protonmail.Client.CreateContacts Source: https://context7.com/emersion/hydroxide/llms.txt Manage contacts by listing existing ones with pagination and creating new contacts. New contacts are created with both a cleartext-signed card (for basic info like name and email) and an encrypted card (for all other details). ```APIDOC ## `protonmail.Client.ListContacts` / `CreateContacts` — Contact Management Lists contacts with pagination and creates or updates contacts whose vCard fields are split into a cleartext-signed card (FN, UID, EMAIL) and an encrypted card (all other fields). ### Method `ListContacts(offset int, limit int) (total int, contacts []*Contact, err error)` ### Method `CreateContacts(contacts []*Contact) (created []*Contact, err error)` ### Request Body for CreateContacts - **contacts** (*[]*`*protonmail.Contact`) - Required - A slice of contacts to create. Each contact contains a slice of `ContactCard`s. ### Example Usage (Go) ```go package main import ( "log" "github.com/emersion/go-vcard" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client // List first 100 contacts total, contacts, err := c.ListContacts(0, 100) if err != nil { log.Fatalf("ListContacts: %v", err) } log.Printf("Total contacts: %d, fetched: %d", total, len(contacts)) // Create a new contact card := vcard.Card{} card.SetValue(vcard.FieldFormattedName, "Bob Smith") card.SetValue(vcard.FieldUID, "bob-uid-001") vcard.AddEmailField(card, &vcard.Field{Value: "bob@example.com"}) encCard, err := protonmail.NewEncryptedContactCard(card, c.KeyRing(), c.KeyRing()) if err != nil { log.Fatalf("NewEncryptedContactCard: %v", err) } signedCard, err := protonmail.NewSignedContactCard(card, c.KeyRing()) if err != nil { log.Fatalf("NewSignedContactCard: %v", err) } created, err := c.CreateContacts([]*protonmail.Contact{ {Cards: []*protonmail.ContactCard{encCard, signedCard}}, }) if err != nil { log.Fatalf("CreateContacts: %v", err) } log.Printf("Created contact ID: %s", created[0].ID) } ``` ``` -------------------------------- ### Programmatic Authentication with auth.Manager Source: https://context7.com/emersion/hydroxide/llms.txt The `auth.Manager` handles credential decryption and re-authentication. It returns an active `*protonmail.Client` and the unlocked OpenPGP private key ring. Ensure the `RootURL` and `AppVersion` are correctly set for the `protonmail.Client`. ```go package main import ( "log" "github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/protonmail" ) func newClient() *protonmail.Client { return &protonmail.Client{ RootURL: "https://mail.proton.me/api", AppVersion: "Other", } } func main() { manager := auth.NewManager(newClient) // username = ProtonMail username // password = bridge password printed by `hydroxide auth` client, privateKeys, err := manager.Auth("alice@protonmail.com", "dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ==") if err != nil { log.Fatalf("auth failed: %v", err) } user, err := client.GetCurrentUser() if err != nil { log.Fatalf("get user failed: %v", err) } log.Printf("Logged in as: %s, keys unlocked: %d", user.Name, len(privateKeys)) } ``` -------------------------------- ### protonmail.Client.ListLabels Source: https://context7.com/emersion/hydroxide/llms.txt Fetches all user-defined and system labels for a ProtonMail account. It also provides constants for system label IDs. ```APIDOC ## `protonmail.Client.ListLabels` — Labels and Mailbox Mapping Fetches all user-defined and system labels. System label IDs are predefined constants used to map IMAP mailbox names to ProtonMail storage locations. ### Method Signature ```go func (c *Client) ListLabels() ([]*Label, error) ``` ### Description This method retrieves a list of all labels associated with the authenticated ProtonMail account. Each label object contains its name, ID, type, and exclusivity status. ### Parameters This method does not take any parameters. ### Returns - `[]*Label`: A slice of `Label` objects, where each `Label` has the following fields: - `Name` (string): The name of the label. - `ID` (string): The unique identifier for the label. - `Type` (int): The type of the label (e.g., user-defined, system). - `Exclusive` (int): Indicates if the label is exclusive. - `error`: An error object if the operation fails. ### System Label Constants The following constants are provided for direct use with system label IDs: - `protonmail.LabelInbox`: "0" - `protonmail.LabelDraft`: "8" - `protonmail.LabelSent`: "7" - `protonmail.LabelTrash`: "3" - `protonmail.LabelSpam`: "4" - `protonmail.LabelArchive`: "6" - `protonmail.LabelAllMail`: "5" - `protonmail.LabelStarred`: "10" ### Example Usage ```go package main import ( "log" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client labels, err := c.ListLabels() if err != nil { log.Fatalf("ListLabels: %v", err) } for _, l := range labels { log.Printf("Label: %s (ID: %s, Type: %d, Exclusive: %d)", l.Name, l.ID, l.Type, l.Exclusive) } // System label ID constants for direct use: log.Printf("Inbox ID: %s", protonmail.LabelInbox) log.Printf("Drafts ID: %s", protonmail.LabelDraft) log.Printf("Sent ID: %s", protonmail.LabelSent) log.Printf("Trash ID: %s", protonmail.LabelTrash) log.Printf("Spam ID: %s", protonmail.LabelSpam) log.Printf("Archive ID: %s", protonmail.LabelArchive) log.Printf("All Mail ID: %s", protonmail.LabelAllMail) log.Printf("Starred ID: %s", protonmail.LabelStarred) } ``` ``` -------------------------------- ### Authenticate and Unlock Key Ring Source: https://context7.com/emersion/hydroxide/llms.txt Performs the final authentication step, fetches user and address data, unlocks private keys, and stores the key ring. Handles TOTP if two-factor authentication is enabled. ```go package main import ( "log" "github.com/emersion/hydroxide/protonmail" ) func main() { c := &protonmail.Client{ RootURL: "https://mail.proton.me/api", AppVersion: "Other", } info, err := c.AuthInfo("alice@protonmail.com") if err != nil { log.Fatalf("AuthInfo: %v", err) } authResp, err := c.Auth("alice@protonmail.com", "protonmail-password", info) if err != nil { log.Fatalf("Auth: %v", err) } // Handle TOTP if two-factor is enabled: if authResp.TwoFactor.Enabled == 1 { if _, err := c.AuthTOTP("123456"); err != nil { log.Fatalf("TOTP: %v", err) } } salts, err := c.ListKeySalts() if err != nil { log.Fatalf("ListKeySalts: %v", err) } privateKeys, err := c.Unlock(authResp, salts, []byte("protonmail-mailbox-password")) if err != nil { log.Fatalf("Unlock: %v", err) } log.Printf("Key ring unlocked with %d private keys", len(privateKeys)) } ``` -------------------------------- ### Handle Encrypted Attachments with ProtonMail Client Source: https://context7.com/emersion/hydroxide/llms.txt Use this to upload, download, and decrypt attachments. It involves generating a session key, encrypting the attachment data, and then handling the encrypted stream. Ensure the client is authenticated and has access to the necessary keys. ```go package main import ( "log" "os" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client att := &protonmail.Attachment{ MessageID: "draft-message-id", Name: "report.pdf", MIMEType: "application/pdf", } f, err := os.Open("report.pdf") if err != nil { log.Fatalf("open: %v", err) } defer f.Close() // GenerateKey encrypts the session key to each recipient's public key if err := att.GenerateKey(c.KeyRing()); err != nil { log.Fatalf("GenerateKey: %v", err) } // Encrypt symmetrically encrypts the file data encData, err := att.Encrypt(f) if err != nil { log.Fatalf("Encrypt: %v", err) } created, err := c.CreateAttachment(att, encData) if err != nil { log.Fatalf("CreateAttachment: %v", err) } log.Printf("Uploaded attachment ID: %s (size: %d)", created.ID, created.Size) // Download and decrypt: encStream, err := c.GetAttachment(created.ID) if err != nil { log.Fatalf("GetAttachment: %v", err) } plainStream, err := created.Read(encStream, c.KeyRing()) if err != nil { log.Fatalf("decrypt attachment: %v", err) } _ = plainStream // io.Reader with decrypted attachment bytes } ``` -------------------------------- ### protonmail.Client.Unlock Source: https://context7.com/emersion/hydroxide/llms.txt Performs the final step of authentication: sets the uid/accessToken on the client, fetches the user and all addresses, unlocks all private keys using the login password and per-key salts, and stores the resulting openpgp.EntityList as the internal key ring. ```APIDOC ## `protonmail.Client.Unlock` — Authenticate and Unlock Key Ring Performs the final step of authentication: sets the `uid`/`accessToken` on the client, fetches the user and all addresses, unlocks all private keys using the login password and per-key salts, and stores the resulting `openpgp.EntityList` as the internal key ring. ### Parameters - `authResp` (*protonmail.AuthResponse) - The response from the authentication. - `salts` (map[string][]byte) - A map of key IDs to their salts. - `mailboxPassword` ([]byte) - The mailbox password. ``` -------------------------------- ### Encrypt and Save Credentials Source: https://context7.com/emersion/hydroxide/llms.txt Marshals, encrypts, and saves ProtonMail auth tokens and passwords using NaCl secretbox. Requires a bridge password key for encryption. ```go package main import ( "log" "github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/protonmail" ) func main() { secretKey, _, _ := auth.GeneratePassword() cached := &auth.CachedAuth{ Auth: protonmail.Auth{ AccessToken: "access-token-xyz", RefreshToken: "refresh-token-abc", UID: "user-uid-123", }, LoginPassword: "protonmail-login-password", MailboxPassword: "protonmail-mailbox-password", KeySalts: map[string][]byte{ "key-id-001": []byte("saltbytes"), }, } if err := auth.EncryptAndSave(cached, "alice@protonmail.com", secretKey); err != nil { log.Fatalf("failed to save auth: %v", err) } log.Println("Credentials saved to ~/.config/hydroxide/auth.json") } ``` -------------------------------- ### Access ProtonMail Calendars and Events Source: https://context7.com/emersion/hydroxide/llms.txt Fetch calendars and their events using `protonmail.Client`. Calendar events are encrypted and require key ring access for decryption. Specify a time range for fetching events. ```go package main import ( "log" "time" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client calendars, err := c.ListCalendars() if err != nil { log.Fatalf("ListCalendars: %v", err) } for _, cal := range calendars { log.Printf("Calendar: %s (ID: %s, Color: %s)", cal.Name, cal.ID, cal.Color) now := time.Now() start := now.AddDate(0, -1, 0) // one month ago end := now.AddDate(0, 1, 0) // one month ahead calEvents, err := c.ListCalendarEvents(cal.ID, start.Unix(), end.Unix()) if err != nil { log.Printf("ListCalendarEvents for %s: %v", cal.ID, err) continue } log.Printf(" Events in range: %d", len(calEvents)) for _, ev := range calEvents { log.Printf(" - EventID: %s created=%d", ev.ID, ev.CreateTime) } } } ``` -------------------------------- ### Import Messages into ProtonMail with hydroxide Source: https://context7.com/emersion/hydroxide/llms.txt Reads raw MIME messages or mbox files, encrypts them, and uploads to ProtonMail via the bulk import API. Supports single files, mbox files, and piping from stdin. ```sh # Import a single .eml file: ./hydroxide import-messages alice@protonmail.com message.eml ``` ```sh # Import an mbox file (multiple messages): ./hydroxide import-messages alice@protonmail.com mailbox.mbox ``` ```sh # Pipe from stdin: cat archive.mbox | ./hydroxide import-messages alice@protonmail.com ``` ```go package main import ( "log" "os" "github.com/emersion/hydroxide/imports" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client f, _ := os.Open("message.eml") defer f.Close() if err := imports.ImportMessage(c, f); err != nil { log.Fatalf("ImportMessage: %v", err) } log.Println("Message imported to Inbox") } ``` -------------------------------- ### auth.EncryptAndSave Source: https://context7.com/emersion/hydroxide/llms.txt Marshals a CachedAuth struct to JSON, encrypts it with NaCl secretbox using the bridge password key, and writes it to ~/.config/hydroxide/auth.json. ```APIDOC ## `auth.EncryptAndSave` — Persist Encrypted Credentials Marshals a `CachedAuth` struct (ProtonMail auth tokens + login/mailbox passwords + key salts) to JSON, encrypts it with NaCl secretbox using the bridge password key, and writes it to `~/.config/hydroxide/auth.json`. ### Parameters - `cached` (*auth.CachedAuth) - The authentication data to save. - `email` (string) - The user's email address. - `secretKey` ([]byte) - The secret key used for encryption. ``` -------------------------------- ### hydroxide auth Source: https://context7.com/emersion/hydroxide/llms.txt Performs a full SRP-based ProtonMail login, stores encrypted credentials, and prints a bridge password for email client configuration. Supports interactive login or using environment variables for the bridge password. ```APIDOC ## hydroxide auth Performs a full SRP-based ProtonMail login, stores encrypted credentials in `~/.config/hydroxide/auth.json`, and prints a **bridge password** that must be used instead of the ProtonMail password when configuring email clients. ### Usage ```sh # Log in with a ProtonMail username (prompts for ProtonMail password interactively) ./hydroxide auth alice@protonmail.com # Output: # Password: (hidden) # Bridge password: dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ== # # Keep the bridge password — it is not stored anywhere and is required for # configuring SMTP/IMAP/CardDAV clients. # To avoid interactive prompts, supply the bridge password via environment variable: export HYDROXIDE_BRIDGE_PASS="dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ==" # List all authenticated usernames stored in auth.json: ./hydroxide status ``` ``` -------------------------------- ### auth.Manager Source: https://context7.com/emersion/hydroxide/llms.txt Programmatic authentication using the `auth.Manager`. It decrypts cached credentials, re-authenticates against ProtonMail, and returns an active `*protonmail.Client` along with the unlocked OpenPGP private key ring. ```APIDOC ## auth.Manager — Programmatic Authentication `auth.Manager` is the central authentication hub used by all three protocol backends. It decrypts cached credentials, re-authenticates against ProtonMail when needed, and returns an active `*protonmail.Client` plus the unlocked OpenPGP private key ring. ### Usage ```go package main import ( "log" "github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/protonmail" ) func newClient() *protonmail.Client { return &protonmail.Client{ RootURL: "https://mail.proton.me/api", AppVersion: "Other", } } func main() { manager := auth.NewManager(newClient) // username = ProtonMail username // password = bridge password printed by `hydroxide auth` client, privateKeys, err := manager.Auth("alice@protonmail.com", "dGhpcyBpcyBhIDMyLWJ5dGUgcmFuZG9tIGtleQ==") if err != nil { log.Fatalf("auth failed: %v", err) } user, err := client.GetCurrentUser() if err != nil { log.Fatalf("get user failed: %v", err) } log.Printf("Logged in as: %s, keys unlocked: %d", user.Name, len(privateKeys)) } ``` ``` -------------------------------- ### Authenticate with ProtonMail Source: https://github.com/emersion/hydroxide/blob/master/README.md Log in to ProtonMail via hydroxide to retrieve emails. A bridge password will be generated and displayed, which is necessary for configuring email clients. ```shell hydroxide auth ``` -------------------------------- ### events.Manager — Shared Event Fan-Out Source: https://context7.com/emersion/hydroxide/llms.txt Manages a single polling goroutine for each authenticated user to efficiently fan out ProtonMail events to multiple subscriber channels. This is used internally to share a single polling loop across different backends. ```APIDOC ## `events.Manager` — Shared Event Fan-Out Manages one polling goroutine per authenticated user and fans out `*protonmail.Event` values to all registered subscriber channels. Used internally by both the IMAP and CardDAV backends to share a single polling loop per user session. ### Method `NewManager() *Manager` ### Method `Register(client *protonmail.Client, user string, ch chan<- *protonmail.Event, done <-chan struct{})` ### Method `Poll(user string)` ### Example Usage (Go) ```go package main import ( "log" "github.com/emersion/hydroxide/events" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client mgr := events.NewManager() msgCh := make(chan *protonmail.Event, 16) done := make(chan struct{}) // Register starts a 30-second polling goroutine (or reuses one if already running) // and delivers events to msgCh until done is closed. mgr.Register(c, "alice@protonmail.com", msgCh, done) go func() { for event := range msgCh { log.Printf("Event ID: %s, %d message changes", event.ID, len(event.Messages)) } }() // To trigger an immediate poll (e.g., after sending a message): mgr.Poll("alice@protonmail.com") } ``` ``` -------------------------------- ### protonmail.Client.SendMessage Source: https://context7.com/emersion/hydroxide/llms.txt Sends an encrypted message by first creating a draft, encrypting the body, and then building recipient packages before sending. ```APIDOC ## `protonmail.Client.SendMessage` — Send an Encrypted Message Creates a draft, builds per-recipient `MessagePackageSet` with internal (PGP-encrypted) and external (cleartext) recipient packages, then calls the send endpoint. ### Description This method facilitates sending encrypted emails through the ProtonMail service. It involves several steps: creating an initial draft, encrypting the message body for the sender's key, updating the draft with the encrypted content, and then preparing recipient-specific packages for encryption and sending. ### Method `SendMessage(message *Message, packages []*MessagePackageSet) error` ### Parameters - **message** (*protonmail.Message) - The message object containing subject, MIME type, and encrypted body. - **packages** ([]*protonmail.MessagePackageSet) - A slice of package sets, each representing a recipient and their encryption details. ### Request Example ```go // Assuming 'c' is an authenticated *protonmail.Client // and 'updated' is a *protonmail.Message object after encryption // and 'pkg' is a *protonmail.MessagePackageSet for a recipient if _, err := c.SendMessage(updated, []*protonmail.MessagePackageSet{pkg}); err != nil { log.Fatalf("SendMessage: %v", err) } ``` ### Response - **error** - Returns an error if the message sending fails. ``` -------------------------------- ### protonmail.Client.ListCalendars / ListCalendarEvents Source: https://context7.com/emersion/hydroxide/llms.txt Fetches the list of ProtonMail calendars and their events within a time range. Calendar event cards are encrypted and signed, requiring key ring access to decrypt. ```APIDOC ## `protonmail.Client.ListCalendars` / `ListCalendarEvents` — Calendar Access Fetches the list of ProtonMail calendars and their events within a time range. Calendar event cards are encrypted and signed, requiring key ring access to decrypt. ### Methods #### `ListCalendars()` Fetches a list of all calendars associated with the client's account. ### Returns (ListCalendars) - **`[]Calendar`**: A slice of Calendar objects. - **error**: An error if the calendars cannot be fetched. #### `ListCalendarEvents(calendarID string, startTime int64, endTime int64)` Fetches events for a specific calendar within a given time range. ### Parameters (ListCalendarEvents) #### Path Parameters - **calendarID** (string) - Required - The ID of the calendar to fetch events from. - **startTime** (int64) - Required - The start of the time range in Unix seconds. - **endTime** (int64) - Required - The end of the time range in Unix seconds. ### Returns (ListCalendarEvents) - **`[]CalendarEvent`**: A slice of CalendarEvent objects. - **error**: An error if the events cannot be fetched. ### Example ```go calendars, err := c.ListCalendars() // ... handle error ... for _, cal := range calendars { now := time.Now() start := now.AddDate(0, -1, 0) // one month ago end := now.AddDate(0, 1, 0) // one month ahead calEvents, err := c.ListCalendarEvents(cal.ID, start.Unix(), end.Unix()) // ... handle error ... } ``` ``` -------------------------------- ### protonmail.Client Source: https://context7.com/emersion/hydroxide/llms.txt The Client struct is the HTTP client for the ProtonMail API. It automatically sets the required X-Pm-Uid and Authorization headers, handles 401 responses by calling ReAuth and retrying once, and optionally logs all HTTP traffic when Debug is true. ```APIDOC ## `protonmail.Client` — Core API Client The `Client` struct is the HTTP client for the ProtonMail API. It automatically sets the required `X-Pm-Uid` and `Authorization` headers, handles 401 responses by calling `ReAuth` and retrying once, and optionally logs all HTTP traffic when `Debug` is true. ### Fields - `RootURL` (string) - The base URL for the ProtonMail API. - `AppVersion` (string) - The application version. - `Debug` (bool) - If true, logs all HTTP request/response bodies to stderr. ``` -------------------------------- ### Manage Shared ProtonMail Events Source: https://context7.com/emersion/hydroxide/llms.txt Manages a polling goroutine per user and fans out events to subscriber channels. Used by backends to share a single polling loop. ```go package main import ( "log" "github.com/emersion/hydroxide/events" "github.com/emersion/hydroxide/protonmail" ) func main() { var c *protonmail.Client // authenticated client mgr := events.NewManager() msgCh := make(chan *protonmail.Event, 16) done := make(chan struct{}) // Register starts a 30-second polling goroutine (or reuses one if already running) // and delivers events to msgCh until done is closed. mgr.Register(c, "alice@protonmail.com", msgCh, done) go func() { for event := range msgCh { log.Printf("Event ID: %s, %d message changes", event.ID, len(event.Messages)) } }() // To trigger an immediate poll (e.g., after sending a message): mgr.Poll("alice@protonmail.com") } ``` -------------------------------- ### protonmail.Client.ListMessages / GetMessage Source: https://context7.com/emersion/hydroxide/llms.txt Lists messages with optional filters (label, pagination, search) and fetches full message metadata. Use Message.Read() to decrypt the message body. ```APIDOC ## `protonmail.Client.ListMessages` / `GetMessage` — Fetch Messages Lists messages with optional filters (label, pagination, search) and fetches full message metadata. Use `Message.Read()` to decrypt the message body. ### `ListMessages` Parameters - `filter` (*protonmail.MessageFilter) - Optional filters for listing messages. - `Page` (int) - The page number. - `PageSize` (int) - The number of messages per page. - `Label` (string) - The label to filter by (e.g., `protonmail.LabelInbox`). ### `GetMessage` Parameters - `id` (string) - The ID of the message to fetch. ### Response (ListMessages) - `total` (int) - The total number of messages matching the filter. - `msgs` ([]*protonmail.Message) - A slice of message summaries. ```