gmail_client

Core Gmail/Google Workspace client for Dart. Send and receive emails via Supabase Edge Functions — framework-agnostic, no Flutter dependency.

Features

  • Send emails with attachments (multipart MIME via Edge Functions)
  • List inbox emails with pagination
  • Fetch full email details with parsed body (text + HTML)
  • Connect/disconnect Gmail accounts via Google OAuth
  • Token management (auto-refresh via Supabase Edge Functions)
  • Synced email storage and retrieval from Supabase

Prerequisites

  • Supabase project with Edge Functions deployed
  • Database tables created (see Database Setup)
  • Google Cloud OAuth credentials configured in org_email_config
  • Supabase Auth with Google provider enabled

Getting started

dart pub add gmail_client
import 'package:gmail_client/gmail_client.dart';
import 'package:supabase/supabase.dart';

void main() async {
  final client = SupabaseClient(
    'https://your-project.supabase.co',
    'your-anon-key',
  );

  final emailService = EmailService(client);

  // List emails
  final result = await emailService.listEmails(maxResults: 20);
  for (final email in result.messages) {
    print('${email.from}: ${email.subject}');
  }

  // Send an email
  final sent = await emailService.sendEmail(
    to: 'recipient@example.com',
    subject: 'Hello',
    body: 'This is a test email.',
  );
  print('Sent: ${sent.id}');

  // Connect a Gmail account
  await emailService.connectGoogleAccount(
    'server-auth-code',
    'user@example.com',
    userId: 'user-uuid',
    redirectUri: 'http://localhost:3000/callback',
  );
}

Security

Do not commit google_web_client_secret to your codebase. The client secret is stored server-side in the org_email_config table and never exposed to the client SDK. The saveOrgEmailConfig() method sends it to the database, but getOrgEmailConfig() does not return it.

Use environment variables or .env files only for non-sensitive values (Supabase URL, anon key).

Database Setup

The package requires two tables in your Supabase project. Full migrations are available in the supabase/migrations/ directory of the enviar_gmail repository.

supabase link --project-ref your-project-ref
supabase db push

Option B: Manual SQL

Run the following in Supabase SQL Editor. At minimum you need these two tables:

org_email_config (required by OAuth token exchange)

CREATE TABLE IF NOT EXISTS public.org_email_config (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_name TEXT NOT NULL DEFAULT 'default',
    google_web_client_id TEXT NOT NULL,
    google_web_client_secret TEXT NOT NULL,
    google_ios_client_id TEXT,
    google_android_client_id TEXT,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- Seed default config (replace with your real credentials)
INSERT INTO public.org_email_config (google_web_client_id, google_web_client_secret)
VALUES ('YOUR_CLIENT_ID.apps.googleusercontent.com', 'GOCSPX-YOUR_SECRET')
ON CONFLICT DO NOTHING;

user_email_tokens (required by all Edge Functions)

CREATE TABLE IF NOT EXISTS public.user_email_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
    email TEXT NOT NULL,
    access_token TEXT NOT NULL,
    refresh_token TEXT,
    token_expiry TIMESTAMPTZ NOT NULL,
    display_name TEXT,
    scopes TEXT[],
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

Optional tables

Table Purpose
synced_emails Local cache of emails (used by getSyncedEmails())
email_sync_history Reserved for future incremental sync

These are not required for core functionality (send, list, get).

API

EmailService

Method Description
listEmails({query, maxResults, pageToken}) List inbox emails. Returns ListEmailsResult.
getEmail(messageId) Get full email by Gmail message ID. Returns EmailMessage.
sendEmail({to, subject, body, cc, bcc, attachments}) Send an email. Returns SendEmailResult.
connectGoogleAccount(code, email, {redirectUri, userId}) Exchange OAuth code for Gmail tokens.
disconnectAccount() Remove stored Gmail tokens.
isEmailConnected() Check if current user has Gmail tokens.
getConnectedEmail() Get the connected email address.
getDisplayName() / updateDisplayName(name) Get/set sender display name.
getSyncedEmails({limit, offset}) Get synced emails from local Supabase storage.
getOrgEmailConfig() / saveOrgEmailConfig(...) Manage OAuth configuration.

Error Reference

Exception When thrown
GmailAuthException OAuth failures: missing user, invalid code, no active config
GmailSendException Email send failures from Gmail API
GmailTokenException Token expired and no refresh token available
GmailConfigException Missing or invalid organization config
GmailClientException Generic errors (invalid response, network issues)

All exceptions extend GmailClientException and expose message, code, and details.

End-to-end Example

import 'package:gmail_client/gmail_client.dart';
import 'package:supabase/supabase.dart';

void main() async {
  // 1. Initialize Supabase
  final client = SupabaseClient(
    'https://your-project.supabase.co',
    'your-anon-key',
  );
  final emailService = EmailService(client);

  // 2. Sign in user via Supabase Auth (Google provider)
  //    (This step uses supabase_flutter or supabase on your platform)

  // 3. Save Google Cloud OAuth credentials (admin step, once per org)
  await emailService.saveOrgEmailConfig(
    webClientId: 'xxx.apps.googleusercontent.com',
    webClientSecret: 'GOCSPX-xxx',
  );

  // 4. User authorizes Gmail scopes → you get serverAuthCode
  //    (via Google Sign-In on your platform)

  // 5. Connect Gmail account
  await emailService.connectGoogleAccount(
    'server-auth-code-from-google',
    'user@gmail.com',
  );

  // 6. Send an email
  final sent = await emailService.sendEmail(
    to: 'recipient@example.com',
    subject: 'Hello from gmail_client',
    body: 'This email was sent programmatically.',
  );
  print('Sent! Message ID: ${sent.id}');

  // 7. List inbox
  final inbox = await emailService.listEmails(maxResults: 10);
  for (final email in inbox.messages) {
    print('${email.from}: ${email.subject}');
  }

  // 8. Read a specific email
  if (inbox.messages.isNotEmpty) {
    final full = await emailService.getEmail(inbox.messages.first.id);
    print('Body: ${full.bodyText}');
  }
}

Troubleshooting

Problem Cause Solution
GmailAuthException: No active OAuth configuration found org_email_config is empty or is_active=false Insert a row with your Google Cloud client ID and secret
GmailTokenException: No tokens found for user User hasn't connected their Gmail account Call connectGoogleAccount() first
GmailClientException: relation does not exist Database tables not created Run migrations via supabase db push or SQL Editor (see Database Setup)
GmailAuthException: User not authenticated No Supabase session Sign in via Supabase Auth before calling email methods
CORS errors when calling Edge Functions from browser localhost differs from supabase.co origin Use flutter run -d chrome --web-port=3000 --web-browser-flag=--disable-web-security for local dev
Token refresh fails Client secret changed or revoked in Google Cloud Update google_web_client_secret in org_email_config

Libraries

gmail_client