Flutter / googleapis / Gmail API send email returns 400 Bad Request

1,331

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")
Share:
1,331
Boyan Bozhidarov
Author by

Boyan Bozhidarov

Updated on December 09, 2022

Comments

  • Boyan Bozhidarov
    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 get DetailedApiRequestError(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
    Herbert Poul almost 4 years
    Since 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
    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.