gmail_client 1.0.1
gmail_client: ^1.0.1 copied to clipboard
Core Gmail/Google Workspace client — send and receive emails via Supabase Edge Functions. Pure Dart, framework-agnostic.
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.
Option A: Supabase CLI (recommended) #
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 |