Skip to content

[Core]: SMTP OAuth authentication (Microsoft 365 / Google) #378

Open

Description

@nvizion

Microsoft recently tightened security and soon will prevent SMTP authentication from other applications.
Instead they require the registration of a App and sendmail and sending via a Office 365 mailbox will then only be possible if the user/mailbox has been authenticated against the App configuration.

Can this be added in one of the next releases?

Activity

  1. nielsdrost7 commented on Apr 20, 2023

    @nielsdrost7
    Collaborator

    It is very difficult to do that in v1
    Would you mind if we do it in version 2?

  2. nvizion commented on Apr 20, 2023

    @nvizion
    Author
  3. nielsdrost7 commented on Apr 20, 2023

    @nielsdrost7
    Collaborator

    Sorry, I had to translate it, plus cut the extra info that was there

  4. nielsdrost7 commented on Apr 20, 2023

    @nielsdrost7
    Collaborator

    Fine, let's wait, any idea when v2 is planned?

    So ... V2. No planning yet.
    The idea is to build it with the help of the community.

  5. changed the title [-]Feature Request - Add option to authenticate SMTP connection using OAuth[/-] [+][Feature Request] Add option to authenticate SMTP connection using OAuth[/+] on Jul 22, 2023
  6. nielsdrost7 commented on Jan 19, 2026

    @nielsdrost7
    Collaborator

    Hey @nvizion InvoicePlane v2 is actively developed at the moment.
    If you want to help, come join the Discord channel here: https://discord.gg/PPzD2hTrXt

  7. nielsdrost7 commented on Jun 13, 2026

    @nielsdrost7
    Collaborator

    Planning Note — v2 Implementation

    Story: As a company admin whose email provider (Microsoft 365, Google) requires OAuth 2.0 for SMTP, I want InvoicePlane v2 to support SMTP OAuth authentication so that I can send invoice emails without relying on deprecated Basic Auth or app passwords.

    v2 context: v2 uses Laravel's Mail with Symfony Mailer. Symfony Mailer supports OAuth2 via league/oauth2-client adapters. The Settings → Email Configuration page needs to expose OAuth2 credential fields alongside the existing SMTP settings.

    Acceptance Criteria:

    • Email Settings has an Authentication Type toggle: Password (existing) / OAuth2 (Microsoft 365) / OAuth2 (Google).
    • OAuth2 fields: Client ID, Client Secret, Tenant ID (for Microsoft), Redirect URI.
    • A "Authorise" button initiates the OAuth2 flow and stores the refresh token in encrypted company settings.
    • Email sending uses the stored OAuth2 token; the token is refreshed automatically before expiry.
    • If token refresh fails, an admin notification is shown: "Email sending failed — re-authorise OAuth2."

    Implementation:

    • Install league/oauth2-client + thenetworg/oauth2-azure (for Microsoft) or league/oauth2-google.
    • MailService::getMailer(Company): build a SmtpTransport with OauthTokenProvider when auth type is oauth2_microsoft or oauth2_google.
    • Encrypted token storage: $company->settings->set('smtp_oauth_refresh_token', encrypt($token)).
    • New route for OAuth callback: GET /oauth/email/callback — outside Filament, in a dedicated controller.

    Test Scenarios (PHPUnit):

    // Settings saves OAuth2 credentials
    /* Arrange */ $data = ['email_auth_type' => 'oauth2_microsoft', 'email_client_id' => 'abc123',
                           'email_client_secret' => 'secret', 'email_tenant_id' => 'tenant'];
    /* Act */     app(SettingsService::class)->updateEmailSettings($this->company, $data);
    /* Assert */  $this->assertEquals('oauth2_microsoft', $this->company->fresh()->settings->email_auth_type);
    // Mail fails gracefully when OAuth token expired and cannot refresh
    /* Arrange */ $this->company->settings()->update(['smtp_oauth_refresh_token' => 'expired_token']);
                  Http::fake(['*/oauth2/token' => Http::response(['error' => 'invalid_grant'], 400)]);
    /* Act */     $result = app(MailService::class)->sendInvoiceEmail($invoice = Invoice::factory()->for($this->company)->create());
    /* Assert */  $this->assertFalse($result);
                  // And a notification is queued for the admin

    Generated by Claude Code

  8. changed the title [-][Feature Request] Add option to authenticate SMTP connection using OAuth[/-] [+][Core]: SMTP OAuth authentication (Microsoft 365 / Google)[/+] on Jun 13, 2026
  9. added this to the v2.0.0-alpha milestone on Jun 13, 2026
  10. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    Collaborator

    Test Suggestions

    Added to support future implementation of this request. The issue body is unchanged.

    config/mail.php's smtp mailer only supports classic username/password auth (env('MAIL_USERNAME'), env('MAIL_PASSWORD')) — there is no OAuth token transport configured, and composer.json has no Microsoft Graph or Google API OAuth package installed. Modules\Core\Models\MailQueue (table mail_queue) already tracks is_sent/error per mailable, which is useful scaffolding for asserting OAuth-specific send failures once a token-based transport exists, but the transport itself is entirely new work — this is not a config tweak, it needs a custom Symfony Mailer transport (or a package like league/oauth2-client + a custom transport class) plus token storage/refresh.

    • PHPUnit: it_has_no_oauth_mail_transport_configured_today() (documents current state)
    #[Test]
    public function it_has_no_oauth_mail_transport_configured_today(): void
    {
        /* Act */
        $mailers = array_keys(config('mail.mailers'));
    
        /* Assert */
        $this->assertNotContains('oauth-smtp', $mailers);
    }
    • PHPUnit: it_refreshes_an_expired_oauth_token_before_sending() (aspirational)
    #[Test]
    public function it_refreshes_an_expired_oauth_token_before_sending(): void
    {
        /* Arrange */
        $this->company->update(['mail_oauth_token_expires_at' => now()->subMinute()]);
    
        /* Act */
        app(MailOAuthTokenService::class)->ensureFreshToken($this->company);
    
        /* Assert */
        $this->assertTrue($this->company->fresh()->mail_oauth_token_expires_at->isFuture());
    }
    • PHPUnit: it_records_a_mail_queue_failure_when_the_oauth_token_is_invalid()
    #[Test]
    public function it_records_a_mail_queue_failure_when_the_oauth_token_is_invalid(): void
    {
        /* Arrange */
        Mail::fake();
        $invoice = Invoice::factory()->for($this->company)->create();
    
        /* Act */
        app(InvoiceService::class)->sendInvoiceEmail($invoice);
    
        /* Assert */
        $this->assertDatabaseHas('mail_queue', ['mailable_id' => $invoice->id, 'is_sent' => false]);
    }

    Edge cases worth covering: token refresh failing mid-send (should not silently drop the email — needs a retry or clear error surfaced to the admin), and per-company OAuth credentials since this is a multi-tenant app (each Company may need its own Microsoft/Google app registration, not one global credential).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions