Flutter / googleapis / Gmail API send email returns 400 Bad Request
An old one but still relevant!
Service accounts (SA) require a real gsuite account to send emails through and which the SA can impersonate.
Solution...First ensure your SA has domain wide delegation and the authorised scope for mail sending (https://www.googleapis.com/auth/gmail.send) in Manage API Access in Admin Console. Then add the impersonated user variable, supplying an existing gsuite email address to the credentials as below:
ServiceAccountCredentials.fromJson(myJsonCredentials, impersonatedUser: "existing_gsuite_email@yourdomain")
Boyan Bozhidarov
Updated on December 09, 2022Comments
-
Boyan Bozhidarov over 1 year
I'm facing issue using the googleapis package for Flutter/Dart. That's the code that I have...
import 'package:googleapis/gmail/v1.dart' as gMail; import "package:googleapis_auth/auth_io.dart"; import 'package:flutter/services.dart' show rootBundle; class Example { ServiceAccountCredentials credentials; Future<gMail.GmailApi> getGMailApi() async { return gMail.GmailApi(await getGoogleClient()); } Future<AuthClient> getGoogleClient() async { return await clientViaServiceAccount(await getCredentials(), [ 'https://www.googleapis.com/auth/drive', 'https://mail.google.com/', ]); } Future<ServiceAccountCredentials> getCredentials() async { if (credentials == null) { credentials = ServiceAccountCredentials.fromJson( json.decode(await rootBundle.loadString('GSuiteServiceAccountInfo.json'))); } return credentials; } String getBase64Email({String source}) { List<int> bytes = utf8.encode(source); String base64String = base64UrlEncode(bytes); return base64StringFormatted; } sendEmail({ String from: 'me', String to: '[email protected]', String subject: 'Some subject', String contentType: 'text/html', String charset: 'utf-8', String contentTransferEncoding: 'base64', String emailContent: '<table></table>', }) async { (await getGMailApi()).users.messages.send( gMail.Message.fromJson({ 'raw': getBase64Email( source: 'From: $from\r\n' 'To: $to\r\n' 'Subject: $subject\r\n' 'Content-Type: $contentType; charset=$charset\r\n' 'Content-Transfer-Encoding: $contentTransferEncoding\r\n\r\n' '$emailContent'), // emailContent is HTML table. }), from); } }
When I call
sendEmail
function I getDetailedApiRequestError(status: 400, message: Bad Request)
. But when I try to send the base64UrlEncoded string through the playground it works, the email is sent.Here is my
flutter doctor -v
:[√] Flutter (Channel stable, v1.2.1, on Microsoft Windows [Version 10.0.17763.316], locale en-US) • Flutter version 1.2.1 at C:\src\flutter-0.7.3\flutter • Framework revision 8661d8aecd (3 weeks ago), 2019-02-14 19:19:53 -0800 • Engine revision 3757390fa4 • Dart version 2.1.2 (build 2.1.2-dev.0.0 0a7dcf17eb) [√] Android toolchain - develop for Android devices (Android SDK version 28.0.3) • Android SDK at C:\Users\bbozhidarov\AppData\Local\Android\Sdk • Android NDK location not configured (optional; useful for native profiling support) • Platform android-28, build-tools 28.0.3 • ANDROID_HOME = C:\Users\bbozhidarov\AppData\Local\Android\Sdk • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01) • All Android licenses accepted. [√] Android Studio (version 3.3) • Android Studio at C:\Program Files\Android\Android Studio • Flutter plugin version 33.3.1 • Dart plugin version 182.5215 • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1248-b01) [!] IntelliJ IDEA Community Edition (version 2018.1) • IntelliJ at C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.1.6 X Flutter plugin not installed; this adds Flutter specific functionality. X Dart plugin not installed; this adds Dart specific functionality. • For information about installing plugins, see https://flutter.io/intellij-setup/#installing-the-plugins [!] VS Code, 64-bit edition (version 1.30.2) • VS Code at C:\Program Files\Microsoft VS Code X Flutter extension not installed; install from https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter [√] Connected device (1 available) • Android SDK built for x86 • emulator-5554 • android-x86 • Android 8.1.0 (API 27) (emulator) ! Doctor found issues in 2 categories.
And this is my
pubspec.yaml
:name: App Mobile description: App Mobile version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter intl: 0.15.7 cupertino_icons: 0.1.2 http: 0.11.3+17 shared_preferences: ^0.4.2 google_api_availability: 1.0.4 geolocator: 2.1.0 file_picker: 0.1.6 url_launcher: 4.0.3 flutter_calendar_carousel: 1.3.10 date_util: 0.1.4 permission_handler: 2.1.0 simple_permissions: 0.1.9 logging: 0.11.3+2 googleapis: 0.52.0+1 uuid: 2.0.0 googleapis_auth: 0.2.7 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true assets: - assets/email_templates/dynamic_form_email_template.html - assets/email_templates/dynamic_form_email_data_row_template.html - GSuiteServiceAccountInfo.json
I exclude the possibility of an issue with the credentials. The same credentials/account/client is working well with DriveApi. Any help will be much appreciated. Thanks.
-
Herbert Poul almost 4 yearsSince this is about a flutter app, and maybe I'm missing something - but is it really a good idea to use service account credentials with that kind of permission on a client side app you distribute to users?
-
plam almost 4 years@herbert Service accounts are useful in accessing services but you are correct there are inherent risks which need to be managed. It's a whole other topic and there are plenty of discussions on SO about security implementation. Essentially assume given enough time nothing is secure and put in place process to deal with the breaches as they occur. In the above code the API credentials are stored as a resource. Whilst this may just be an example, its definitely not the way to start.