Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Add support for Gmail's XOAUTH2
I really do not want to allow "less secure apps" in my gmail account.
Would K9Mail be compatible with this gmail security some time in a future?



Discussion hints:
"Less secure apps", as far as I'm aware, really just means apps that don't use Google's proprietary APIs, rather than using standards like K-9 does.

You can also use an "app-specific password" created from your Google account, which is a part of their two-factor authentication system, which you should be using anyways.

On "less secure apps": https://www.google.com/settings/security/lesssecureapps
On "app-specific passwords": https://support.google.com/accounts/answer/185833

See https://developers.google.com/gmail/xoauth2_protocol

Perhaps this is the reason I periodically receive an e'mail from the Gmail Team stating, "Someone has your password!".  So much dreck is peddled by Google.

Implementing XOAuth2 for Google will require us to:
- Obtain a client ID and 'Secret' for K-9 (which will have to be in the APK so may as well be in the repo).
- Implement a screen for users to allow K-9 access to their email (or just a link to an external browser window)
- Implement a follow-on screen to accept the verification code and generate an access code and refresh code.
- Store the refresh code and action codes as their pseudo-passwords
- Use the action code to login (this is quite similar to CRAM_MD5)
- Use the refresh code to periodically refresh the access code (1 hour for Google)

Some of this is common across anyone who uses XOAuth2. But much of it is Google's implementation. It may or may not be the same for different providers. While I'd want the developer to do it in a provider neutral fashion as possible, until you have two it's impossible to know what the changes will be - they could have no concept of token refresh for example.

Whether putting the "client secret" in a public repo is acceptable to Google is a good question.

I would hope so, otherwise it's impossible to use the api for email clients, which sort of defeats the purpose. 

As a first step we probably want to limit ourselves to Google accounts set up on the device. Then we can use `AccountManager.getAuthToken(…)` to retrieve the tokens.

If you limit yourselves to Google accounts set up on the device, would that allow for access to both the free consumer Google account signed into the Android device as well as additional Google for Work accounts?  I have a K-9 devotee in the family that recently switched their primary email to a Google for Work account and they are being plagued with the "Sign-in attempt prevented" emails and I've been tasked with finding a solution.

The work-around is to use the app-specific passwords @ocdtrekkie mentioned.

As for whether the unimplemented future solution will work for additional GfW accounts, I have no idea - not being sure how they get integrated into a phone. If they get integrated as Google account on the device possibly, depending on the implementation. It's all hypothetical at this point as I've not done much more than the initial look at the protocol.

There's a new [xoauth2](https://github.com/k9mail/k-9/compare/master...xoauth2) branch from work I've done so far.

Here's what works.
- You can now select to authenticate incoming using an OAuth 2.0 token.
- If you do that it will use the AccountManager to try and fetch a token for the email address you provide.
- The IMAPStore now checks for capability for authentication via XOAuth 2 and SASL-IR. 
- For authentication it will get a token, and then using the token it will make a XOAUTH2 AUTHENTICATE request. It will handle the continuation response and parse the status response.

Here's what doesn't work/needs work:
- Authentication with Gmail never actually works. So currently you can't get further than account setup :( I have no idea why right now. Google's documentation is a maze and none of it quite covers the interaction between the AccountManager Android API and the GMail XOAUTH authentication. The token type may be wrong (token types are not documented on Google's site, at all - I found a reference to the "mail' token type and it looked right...) or you may need to pass in an Application API Key or you may not be able to auth like this...
- If you are going to use a token you shouldn't need to put in a password. Currently you do. Choosing token auth may well need moving to the email address page which will then need a re-design.
- We can get a list of Google Accounts. So we should do that and allow a user to select the one they want to authenticate for rather than having to type the address and match it (e.g. gmail vs googlemail). This can be part of the re-design of the email address page.
- SMTP doesn't support XOAUTH2 at all yet - I've not looked at it.
- The AccountManager currently gets passed in to the ImapConnection class - like the Connectivity Manager. We probably want a more general AuthTokenProvider.

Think of this branch as like #884 for XOAuth 2. It's not the solution, but it's at least a starting point for someone (maybe me) to get it to work.

Okay, updated branch with some help from cketti regarding finding correct auth type.

Of the list of not working stuff:
- Authentication with Gmail works
- Selecting it is now done in the 'advanced options'. 
- When selected, the password fields are hidden and the username field turns into a drop down of the accounts registered.
- Automatic configuration of Gmail accounts now uses XOAUTH2
- SMTP now supports XOAUTH2
- There's an OAuth2TokenProvider interface implemented by the AndroidAccountOAuth2TokenStore which manages tokens

What still needs to be done:
- I need to test token expiry timeouts. I don't want requests to fail periodically, the token get invalidated then the next request works fine. We need to ensure this gets handled gracefully. This might mean tracking invalidation times and invalidating it and getting a new one when it's close.
- I need to test removing the privelege.
- I need to workout how to provide the Application Key
- It'll all need documenting in the manual

So it's working in a very rough state. I wouldn't recommend it unless you're familiar with debugging though just yet.

Testing blocked by
- [ ] Support different pre and post-auth capabilities. #1297

I've not run into the capabilities bug recently (I applied cketti's fix). So I've been getting on with testing.
- I've written code that seems to behave correctly for token refresh
- I've started testing non-Gmail accounts ( @AndyCouch ). Not run into any issues so far.

Thought I'd share some screenshots :) 

In the first you can see OAuth 2.0 has been checked and the account list is now a drop-down with the list of available Google Accounts. The password field is gone and client-certificate option is greyed out.

In the second you can see the authentication screenshot that appears. Note currently K-9 is an unregistered app. I need to fix this next because I can't work out how to de-auth an unregistered app and if I can't do that I'll be signing up to lots of Gmail accounts :)

![screenshot_20160419-022735](https://cloud.githubusercontent.com/assets/1316415/14625394/9886fec4-05da-11e6-907a-75c0a26fbe46.png)
![screenshot_20160419-024046](https://cloud.githubusercontent.com/assets/1316415/14625396/99bfd72a-05da-11e6-8524-13fabbfe6d7f.png)

@cketti Can you please obtain an API key for this from Google. https://console.developers.google.com/ - if we don't already have a project, create one, search for Gmail API, fill in the consent screen and then obtain the key (you may need to get a SHA1 of the certificate 

Hopefully doing one for probably actually want one for com.fsck.k9 is enough and we don't need a separate one for com.fsck.k9.debug.

(I'm asking you because it wants to be done under the same account that holds the Play store stuff I think)

Done.

Thanks. Can you either email me the key (and secret if there is one) or commit it to the repo? They get added to the request sent to Google in the app. 

@cketti Waiting for the above to finish this off. I think it's pretty much done otherwise.

Any news regarding this issue or an ETA when it'll be implemented in the F-Droid version of the app?

 I think we need to be realistic and understand that if Google implements standards of its own, it is not fault of K9. K9 is the best open-source email app out there and follows all standard email protocols and standards in most transparent manner (how many other apps offer such flexibility).

So if you're serious about not using the Gmail app, then you should not be using Gmail email server either. I have my own email server and bumped into this thread, as my employer uses Google Apps and I wanted to have all my phone email served by K9.

As @ocdtrekkie pointed out
"_You can also use an "app-specific password" created from your Google account, which is a part of their two-factor authentication system, which you should be using anyways._"

There is everything explained here: https://support.google.com/accounts/answer/185833
Only change is that you use generated password instead of your Google Account usual password, when setting up in K9.

When will this be released? 
The branch of code that implements this works (mostly). But the UX is pretty bad and it can't support non-Google XOAUTH implementations. So that needs to be worked on. Making a half-baked solution that only supports Google isn't much good if it makes the others ones harder to do.

Right now we're pretty busy stabilising the current release. At the point that stops or at least becomes manageable I will try and get back to it.
Oh, ok -- I thought because you added the "next stable" tag it'll actually be in the next stable release.
While I agree it's always great to have a very general solution, often this is very costly to achieve. Of all oauth support requests gmail is the one the most users will want to have (easily 90% and more), so if you get this running then making it more general also can be done later. As one of the comments here said: These oauth things are proprietery for all providers -- so most likely you'll have to implement it again each time you want to support another provider :/
I certainly hope it'll be in the next major stable (not a bug fix release) now that PGP/MIME is mostly there. 

Initially I did hope that doing it for Google alone would be sufficient. But the big providers are all moving to a post password era following numerous incidents. So it makes no sense not to do it properly once.

There is actually more in common in the back end than you might expect - it's just the initial setup that differs really because only Google uses the Android framework - MS and Yahoo both want you to visit a web page.
What's the status of the OAuth support? I would have thought it would be in 5.205, but it's not.
Per my last comment basically.

The 5.2 code (e.g. 5.201, 5.205) contains the PGP/MIME stuff and associated bug fixes.
A future branch (possibly 5.4 though there are other priorities like handling Doze) will hopefully contain a rework of the on-boarding (account creation) process, including OAuth.
Has something changed on the google side recently?   My phone was using a google OTP I had generated 2-3 years ago I think (and which survived several phone-to-phone migrations using Titanium Backup.  However, I just tried to set up K9 on a new phone and generated a new OTP from my account.  However K9 is responding with wrong password error.  Anyone else seeing this?
Please add support of XOauth. It's very important feature. Even less secure access is enabled in the Google account,  i'm unable to read mails once I change country or use new public wifi, because Google is requesting login via browser to confirm ebery new unsecure login 
@devbazilio It's generally considered impolite on GitHub to make a comment just to say you also want to see a feature. There is already an issue here to request the feature, your comment adds nothing to the discussion. The best way to show your support for a feature is to look at the top right of the original issue for the like + :) icon, and add a thumbs up (:+1:) to the issue.
update: I made a mistake -- keystore doesn't need to be matched any more if not depend on Android account system. So anyone looking forward to use gmail on K-9 can just clone K-9, checkout to [my branch](https://github.com/k9mail/k-9/tree/GSOC_account_setup), build it from source and install it on phone. OAuth 2.0 of both Gmail and outlook are working now.

--------

Some progress: Now oauth 2.0(no dependency on Android account system) gets working on my [GSoC branch](https://github.com/k9mail/k-9/pull/2580)(Based on philip's amazing work). But because I used my own debug.keystore to register on Google API Console so only myself can use it for the time being. Please wait for it being merged :)

Screenshot:

![k9oauth2-1](https://user-images.githubusercontent.com/11607199/29443984-87ef0e2c-840e-11e7-84fa-6d53c33c7ecd.png)
![k9oauth2-2](https://user-images.githubusercontent.com/11607199/29443985-87f03d10-840e-11e7-9c90-561c6b4d05a9.png)
![k9oauth2-3](https://user-images.githubusercontent.com/11607199/29443172-dc0698e6-8408-11e7-8f4a-3bfd6e35e57e.png)



@philipwhiuk more than happy to test anything that you may have. I am essentially still using the Gmail application because of my work email requiring Oauth2, and the admins not knowing how to enable application specific password for their users... This would be a huge time saver for my daily workflow. Thanks!
New user here, but this might be more important now. I tried logging in on a new GSuite account/domain **using an app password** and I get the error seen in this [issue](https://github.com/k9mail/k-9/issues/1538). Google also emailed an alert to my account saying they blocked the login. 

Google has this text on their [help page](https://support.google.com/accounts/answer/185833?hl=en&authuser=1) for App Passwords (under 'Still can't sign in'):

> If you're using an app not made by Google and can't sign in, that app's sign-in process might not be secure. If there's a corresponding Google app, we recommend using it instead. For example, if you can't sign in to an email app, use the Gmail app.

I'm not sure what the point of App Passwords is anymore if Google is blocking them, but they are. 

My existing Google accounts don't have this issue, and a test account I created on *my* older GSuite domain worked fine, so whatever new policy this is isn't being fully rolled out yet.

In the meantime is there a workaround to using K9-Mail on my phone with my organization's account? Google does have an option to allow access to less secure apps, but 
1. My new organization needs to enable it.
2. From what I saw with my (unaffected) GSuite account, you *also* have to **disable** 2-factor authentication on your account! 
> New user here, but this might be more important now. I tried logging in on a new GSuite account/domain using an app password and I get the error seen in this issue. Google also emailed an alert to my account saying they blocked the login.

This works for myself and others (2FA + app password) so it must be related to your set-up.  Are you sure you're following the documentation for connecting?
Yes, I am. I tested it on a new account on my old domain where it works fine (2FA + app password); it's only on the new domain that those steps fail. 
Guys, any updates?
3 years have passed and no still any solution. I stopped using of K9 on phone because I don't want to give a phone number for google. And enabling of less secure apps don't works because Google bans my mailbox at each new IP and asks to login via browser and verify identity by solving captchas/entering other annoying stuff.

It's a very crucial issue.
Same here, reason why I stopped using K9. 
This is one of the reasons I stopped using Gmail. And Android. And K9. :|
> It's a very crucial issue.

If it's that crucial for you, I'd recommend [contributing](https://k9mail.github.io/contribute.html).
Well there are organizations that let you enable Less secure applications, but not 2FA, my organization's settings are like that despite repeated request to turn on 2FA + application specific passwords. Thunderbird now seems to favor defaulting to OAuth2 for my personal gmail.com address, so this could likely be the future at some point.
Could there maybe be unstable F-Droid releases with gmail support ?
> This works for myself and others (2FA + app password) so it must be related to your set-up. Are you sure you're following the documentation for connecting?

I've just changed work-place and I'm unable to use the app password (I have the same setup as my old work-place where the app password still works).

For example, the error for SMTP authentication is:
```
AUTH PLAIN failed (Username and Password not accepted. Learn more at https://support.google.com/mail/?p=BadCredentials b129sm9992323wmd.24 - gsmtp)
```

Just confirming what others have observed that this is likely becoming more and more common.
Hi guys. Any news on XOAUTH  support? Google keeps buzzing me with suggestions to disable access for less secure apps and K9 is the only one that keeps me from doing so. Thanks and keep up the good work.
Use pep instead of k9. It's a fork of k9 and has this feature finally implemented: https://pep.software/android/ – k9 seems to be dead.
@DreamFlasher Thanks for the suggestion. I have the same issue

@mdcclxv Another option is to create an application specific password for K9
@DreamFlasher 
it works if my personal account was added to device but I'd like to don't that as all bloatware on phone will see and can access my account. I'm now using Bluemail. It's a closed source app, but it doesn't require ore-added google accounts to phone.

@paul70078 app passwords work if you had set up a phone number to google account, I don't want to do that as all stupid sites now want to know my phone number nowaday
@DreamFlasher I don't know if I'd call it dead, there has been commits in the last month. But maybe the feature could be merged back from pep
@DreamFlasher Why was there a fork?  I can't find documentation on this anywhere.
This is not a discussion forum. Please use the [mailing list](http://groups.google.com/group/k-9-mail).
This looks to have been fixed in the e foundations Mail fork here: https://gitlab.e.foundation/e/apps/Mail
> 
> 
> This looks to have been fixed in the e foundations Mail fork here: https://gitlab.e.foundation/e/apps/Mail

Link on Google Play?
It's not on Google play (you'd have to build the app from source yourself to use it at the moment I think), but thought it might help someone fix this quicker to look at their solution.
I'm not installing some third party app from an unverified source, sorry.
Do not see in current v5.600 that OAuth2 :(
> This looks to have been fixed in the e foundations Mail fork here: https://gitlab.e.foundation/e/apps/Mail

Are K9 guys attempting to backport those changes made by /e/?
Probably all /e/ has done just was simply slapping their branding on top of the gmail-branch
I think one option could be to create an oauth client id and secret for every install in that particular Gmail users account. Then k9 mail can access that particular user's Gmail using the newly generated client id and secret from that particular user's account.

The process for generating a client id and getting the secret is outlined here - https://medium.com/@pablo127/google-api-authentication-with-oauth-2-on-the-example-of-gmail-a103c897fd98

K9 mail can easily scrape a simple web page with or without user interaction and retrieve the client secret. Or the user can be asked to paste the client secret.

Most users will be within the allowed limit of 100 oauth accesses for unverified apps.

Effectively make every user his own developer also!

Do you think this is a good idea?

Your thoughts please? Thanks!
@Serkan-devel, when you mentioned a gmail-branch, is it https://github.com/k9mail/k-9/tree/xoauth2 ?
As a note, secure passwords are being deprecated in June 2020 for G Suite accounts.
If this isn't implemented by then then I won't be able to use K9 any more. (Along with, presumably, a fair number of others)
To be honest, FairEmail (https://email.faircode.eu/) has OAuth support already via wizard and has far more flexibility in settings. Also encrypted emails are supported. Also developer is very responsive and considers development proposals quite quickly.
I too will have to move on.  This topic has been open for 4 years.  Maybe K9 has finally had its day. :(
At least we are not alone - I can confirm that FairEmail (https://email.faircode.eu/) is very strong application and good replacement and worth to try. Especially I like responsive developer and most of my proposals are already accepted and already deployed. Certainly these proposals will be weighted but will be added to todo list as soon as they will be accepted. Relatively fast release cycle and beta releases are working already as stable (ready to be deployed to Google Play and available also in F-Droid). I am very satisfied and also the donation for developer was relatively reasonable if to count what I just described. Donation helps to unlock pro features (https://email.faircode.eu/donate/), e.g. biometric (e.g. fingerprint) authentication, etc.
I could easily add even Hotmail account via wizard, using "Other provider" choice.
Gmail support in fairemail available only on google play build
While I can empathize with people using a different mail system, what I'm looking for is the functionality specified in this issue, for this application.
**This is now required.  From Google to all Gsuite users dated December 16, 2019:**
_Starting February 15, 2021, G Suite accounts will only allow access to apps using OAuth. Password-based access will no longer be supported.

Dear Administrator,

We’re constantly working to improve the security of your organization’s Google accounts. As part of this effort, and in consideration of the current threat landscape, we’ll be turning off access to less secure apps (LSA) — non-Google apps that can access your Google account with only a username and password, without requiring any additional verification steps. Access through only a username and password makes your account more vulnerable to hijacking attempts. Moving forward, only apps that support a more modern and secure access method called OAuth will be able to access your G Suite account.

Access to LSAs will be turned off in two stages:

    June 15, 2020 - Users who try to connect to an LSA for the first time will no longer be able to do so. This includes third-party apps that allow password-only access to Google calendars, contacts, and email via protocols such as CalDAV, CardDAV and IMAP. Users who have connected to LSAs prior to this date will be able to continue using them until usage of all LSAs is turned off.
    February 15, 2021 - Access to LSAs will be turned off for all G Suite accounts._

> @Serkan-devel, when you mentioned a gmail-branch, is it https://github.com/k9mail/k-9/tree/xoauth2 ?

@je-vv does any other mail-provider have xoauth?


Anyways I prefer a stable client, which won't change much and I won't jump ship to any of these outdated forks that won't contribute their improvements to upstream
side-question: when/if k-9 gets xoauth-support, does that necessarily mean the account will be taken from android settings?

Tried fairemail yesterday shortly, and this only allows for gmail-accounts which are setup-up *within* the system. 


I have two google accounts: one personal for gmail etc, and another one exclusively for android use. I have been using k-9 to prevent that my personal account is being associated with the one I run the device with (--> privacy concerns). So fairmail is out.

Is it technically possible to implement xoauth in a way that is not depending on accounts which are stored in android itself?
It is a lot easier to implement it when you rely on android to handle most of it.
> It is a lot easier to implement it when you rely on android to handle most of it.

Relying on system-wide account management makes implementation easier, but it also prevent me using built-in mail client due to privacy reason as @0x4a said (I have 4 personal Gmail account :/ ). I would also looking for an implementation without Android's account management.

By the way, there are apps like Island/Shelter that utilize Android works profile to isolate other apps in another environment. You need to install another copy of k-9 in the sandbox.
Although those isolated accounts still show in accounts, those accounts have no access to data outside sandbox.
@Serkan-devel, I would have preferred to get k9 with the support on production, and available under f-droid, but who knows when it'll get there...  For the /e/ K9 fork, you can read about the /e/ default app [wiki](https://doc.e.foundation/what-s-e#2-description-of-e-default-applications), and the fork [repo](https://gitlab.e.foundation/e/apps/Mail).  It says to fork from K9, but not if from master or a particular branch...

I'm not using /e/, since I'm still using [LineageOS for MicroG](https://lineage.microg.org), so I can't tell how to download its apk, and how to keep it up to date, other than by installing and using /e/, which I wasn't planning to do.
> Tried fairemail yesterday shortly, and this only allows for gmail-accounts which are setup-up _within_ the system.

Have to admit that it is true.

> I have two google accounts: one personal for gmail etc, and another one exclusively for android use. I have been using k-9 to prevent that my personal account is being associated with the one I run the device with (--> privacy concerns). So fairmail is out.

I can confirm that FairEmail is perfectly suitable for that purpose to separate different Google Mail mailboxes. I have many different accounts from Google: both GMail and G Suite with organization accounts. You need to disable syncing as soon as possible, presumably turn off internet connectivity before finally adding account - then you have time to make all syncing inactive.
Haven't used it, but [FAQ 111](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq111) says that it still uses Android account manager for OAuth even if you use the wizard.

>(111) Why is OAuth not supported?
>
>OAuth is supported for Gmail via the quick setup wizard. The Android account manager will be used to fetch and refresh OAuth tokens for the selected account.
@edmundlaugasson as long as the account is stored in android, I do not trust google enough to *not* connect the accounts given. If they have both of the accounts (e.g. the mail address), that is absolutely enough to connect data of both, even if syncing is off.

Call me paranoid, but I don't want to have my contacts, my maps-data and my mobile searches connected with that other account, which basically has all other data there is about me...

If one of the accounts only "exists" within k-9, it is still possible, but much less likely to do that. So on with the topic: implementation of xoauth in k-9 :) 
@0x4a and other similar people...
Use [/e/](https://e.foundation/) or [Librem 5](https://puri.sm/products/librem-5/) or whatever else similar - nobody says that you should stick with Google?!?!? But as OAuth has its own requirements binded with Google - you should probably avoid Google Mail at all. Use [ProtonMail with .ch](https://protonmail.com/support/knowledge-base/what-is-the-difference-between-protonmail-com-and-protonmail-ch/) or [Librem One](https://librem.one/) - be free from Google! There are also many [other search engines available than Google](https://alternativeto.net/software/google-search/)...
I personally use posteo.de for mail andduckduckgo for search. But my old gmail fetches mails of about ~5 different other accounts and automatically labels and filters them accordingly. It's hard to get rid of that. So I want to keep it, as Gmail is quite comfortable.

Anyway, I'll wait if/when xoauth comes to k9, and see how I'll go on from there...
Even I'm not paranoid, it's still annoying seeing lots of accounts never used in account picker every time. <br>  
Moreover, does the Accounts-Manager solution work with devices without gapps installed? I mean, can I add Google account to device that don't have gapps? I don't have such device so I have no idea.

On 2019年12月20日 1:17:17 GMT+08:00, Edmund Laugasson <notifications@github.com> wrote:
>Use [/e/](https://e.foundation/) or [Librem 5](https://puri.sm/products/librem-5/) or whatever else similar - nobody says that you should stick with Google?!?!? But as OAuth has its own requirements binded with Google - you should probably avoid Google Mail at all. Use [ProtonMail with .ch](https://protonmail.com/support/knowledge-base/what-is-the-difference-between-protonmail-com-and-protonmail-ch/) or [Librem One](https://librem.one/) - be free from Google! There are also many [other search engines available than Google](https://alternativeto.net/software/google-search/)...
>
>-- 
>You are receiving this because you are subscribed to this thread.
>Reply to this email directly or view it on GitHub:
>https://github.com/k9mail/k-9/issues/655#issuecomment-567581105
Sounds like OAuth support is coming also to F-Droid version of FairEmail. [Here is appropriate commit](https://github.com/M66B/FairEmail/commit/f496a0fa6ca9c003140f9b6c16ae586b4494efa0).
Since no one linked to it, here is the official announcement about the change to GSuite policy about password-based authentication: https://gsuiteupdates.googleblog.com/2019/12/less-secure-apps-oauth-google-username-password-incorrect.html
> Since no one linked to it, here is the official announcement about the change to GSuite policy about password-based authentication: https://gsuiteupdates.googleblog.com/2019/12/less-secure-apps-oauth-google-username-password-incorrect.html

As @edmundlaugasson and @physkets has already posted this, I won't repeat it.
What i wll add is that I am happy to donate towards having this implemented.



so, since today, k9 mail not possible to be used for gmail accounts ... as no oauth2
seems to be i need to look for another application - very sad about :(
@camelcamro - there are alternatives - https://alternativeto.net/software/k-9/?platform=android - haven't tested them all but I'm using [FairEmail](https://email.faircode.eu/) for OAuth 2.0
If anybody has a time to test any other Android email client against OAuth 2.0 - please let also us to know here!
Is that OAuth requirement only for GSuite users?  I'm no GSuite user, but I do use GMail.  Also, wasn't June 2020 the month of disallowing new users that haven't before use LSAs, and February 2021 the month to definitely turn it off, according to [gsuite-bblog](https://gsuiteupdates.googleblog.com/2019/12/less-secure-apps-oauth-google-username-password-incorrect.html)?  I'm still wondering if for those regular non gsuite users, this will affect at all, at least in those announced dates.  Wouldn't K9 try a 1st quick approach, perhaps similar to what /e/ mail fork did, and then provide a different more appealing one?
yes, you are right ...
the normal domain @gmail.com still working
but the gsuite for my company with domain @ro****.com not working anymore.
so, i had to change now to FairEmail ... well, it is different, but i have to live with that for now :(

very sad, that it was totally forgotten to be able to handle the OAuth2 to support gmail :(
but that shows that k9 mail is not taking care ... and i would support the team more when possible ... well, i bought kaitenmail as the devels are behind ... but well, maybe it is time to migrate to a product which can deal with the future ...
gsuite works as well. Unless your admin disallows it.
> @camelcamro - there are alternatives - https://alternativeto.net/software/k-9/?platform=android - haven't tested them all but I'm using [FairEmail](https://email.faircode.eu/) for OAuth 2.0
> If anybody has a time to test any other Android email client against OAuth 2.0 - please let also us to know here!

@edmundlaugasson please ... NO

NO, we should not discuss alternatives in an issue tracker, please use [a search engine to find alternatives to K-9 and its derivates Kaiten, K-@ and Bird](https://www.google.com/search?q=E-Mail+Clients+Android+K-9+Kaiten+K-%40+Bird)

NO, you should not use most of them listed and you should have a look at their agreements, since most "fancy" clients are no real clients like k-9 but cloud services

anyone else: complaints doesn't help, there is a pending PR #3804 from @wiktor-k aside the already discussed work done by @philipwhiuk in late 2016 with no PR https://github.com/k9mail/k-9/compare/xoauth2
based on backend commit https://github.com/k9mail/k-9/commit/7774ebc788c8e33ac7994b6cea6614cbd4f68d81 since 5.300 from PR #1295
But indeed, THIS issue contains all the relevant discussion about, e.g. https://github.com/k9mail/k-9/issues/655#issuecomment-211094050

if it doesn't take the attention and drive you hope to get, it's a mostly a sign of too many "unthankful participants" and too few contributors.

yelling goes [to forum please](https://groups.google.com/group/k-9-mail), else you nag those developers that are interested in technical "issue" discussions killing any fun in contributing for an OpenSource project, thank you
It would be nice to see Dev's offer reassurance.  

Unfortunately many may not have known better than to use certain providers, [but information is available](https://www.privacytools.io/providers/email/).  There are even mailto: protocol forwarding services whose MX Records point to whatever other service you like.  

But if there is **legit**' **UNCERTAINTY** it's not "*FUD*" and should be addressed in terms of **SOLUTIONS**.  I disagree with telling someone to go scour some web search, especially not even suggesting Searx, nor [offering a source of info'](https://www.privacytools.io/software/email/). 

But this seems to be an **ISSUE** for **THIS PROJECT**, and thusly in it's issue tracking. 

`¯_(ツ)_/¯`
It appears that later this year Microsoft will be going down the same route with Office 365 as Google is doing with GSuite , and[ requiring OAUTH 2 capability for all clients](https://www.theregister.co.uk/2020/02/26/disruptive_authentication_change_coming_to_exchange_online_microsoft_details_help_for_admins/).




I tried FreeMail from F-Droid and it won't load Gmail

Kinda useless for this case
Adding support for Oauth in K9 is not REQUIRED to keep using it after february 2021. There is an alternative way to keep using K9 after "non secure apps" is definitively disabled next year. I have tested it today:

App Passwords
https://support.google.com/accounts/answer/185833?hl=en

It works also for Apps domains accounts, with one trick: 2steps needs to be enabled by the domain admin, and this will immediately disable less secure apps for the whole domain; there is no smooth migration possible; you must, in this order:
- login as admin
- enable 2steps for admin (immediately disables less secure app for everyone)
- set 2steps for admin
- enable 2steps for domain
- login as user(s), enable and setup 2 steps
- generate AppPasswords
- reconfigure all your apps

If reconfiguring all your app takes hours, then, your services may be down for ... a few hours.

I have migrated one domain today, and it took me 3h for one domain, 2 users, and 3 apps. Next week I have an other domain with 4 users, 30+ apps, and I don't think I can do it within 2 days.

Note that for K9, the password must be updated in incoming AND outgoing menus; the same password works at both places (was not trivial, had to be checked).
@doublehp The main issue is for GSuite accounts, and users are very likely not their own admins. Organisations (like my own) very often decide to not allow app passwords (mostly because it defaults to off). So this is absolutely necessary.
I can't see anything from Google saying that app passwords will still be useable after the deadline, only that OAUTH must be used.

The deadline for switch over has been withdrawn though due to COVID, and no new date announced yet.
Starting from today, it seems that I can't send emails anymore from K9mail on my gmail account... Not sure if they are disabling accounts progressively, but I can't find any way to send emails from K9mail now (when I try to send an email via K9Mail, the email is put instead in the draft folder, and I get a notification saying my email can't be sent, and when I reconfigure the smtp server, it keeps telling me to connect to my browser). Before I just needed to go on my browser and recognize the device, now it's not working anymore.

Note that I don't want to enable 2 factors authentication (don't want to be locked out of gmail if I don't have my phone, and SMS text are anyway simple to bypass if someone really wants access to my mail), so app password is not a solution for me.
> Note that I don't want to enable 2 factors authentication (don't want to be locked out of gmail if I don't have my phone, and SMS text are anyway simple to bypass if someone really wants access to my mail), so app password is not a solution for me.

Buy a Yubikey (or other U2F device) and use it instead. The only accounts I have SMS 2FA enabled on are those that *only* support 2FA via such a route. Everything else is TOTP where I keep secrets on an encrypted USB key (can't even read it with the phone) and NFC-enabled Yubikeys. If you get multiples, you can have backup keys to use for access in case you lose your main one (I have a number in geographically diverse places).
Well I could if I wanted maximum security, but in my case I just want an easy access to my email. So if someone steals my bag in a foreign country, I want to be able to take a random computer and connect to my email. And I guess most people don't want to buy a Yubikey just because they want to read their mail on their phone.
With 2FA, I have app passwords for now. I suspect eventually we'll need OAuth tokens for K9 which still won't involved anything like that in the email workflow.
yahoo also bothering "...so we need you to make some changes before 20 October 2020."
Any news here? Can't seem to change yahoo to Oauth!? 
My university email wont support anything but oauth2 come november 2nd. Is there an open source android mail client that does support it?
I searched on F-Droid and found [FairEmail](https://email.faircode.eu/) which has [this FAQ question](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq62). Skimming the list from a search didn't turn up too many other promising candidates, but I admit I didn't do much digging.
The app seems useful, but it seems that you need to use Google Play's version for XOAUTH usage? Something about Android checking app signatures or the like.

Edit: [This FAQ entry](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq111) seems relevant. Seems that to use OAUTH for Google accounts, one will need to use a Google Play-distributed build of the apps.
It also seems that the account needs to be known to Android generally, so just adding an alternative account to an app means tying your device to it in some way. This is likely how any app would end up working.

I guess all I can recommend is to migrate from GMail to elsewhere (I've been migrating to Fastmail this year) since it seems like these problems are more related to Google locking things down than anything the FOSS community can reliably address.
@mathstuf I fully recommend people switch to Fastmail (I switched in 2016), but FWIW, this isn't really an unobtainable goal for open source apps to support, and this issue was opened six years ago. There's a point where you just say a project is abandoned from a major support standpoint and move on.
@ocdtrekkie There is an open pull request for it.  #4832 , so I will not think it is so unobtainable. Just needs to be checked and merged. There is also #3804. 
#3804 has been hanging around since the beginning of the year. I'd kind of hoped it would have been merged by now.
One of my email providers just changed their policy to require oauth2, and it seems they have also disabled app passwords.  So I am forced to drop K9 mail as after so many years oauth2 support still has not been implemented.  Looks like a possible alternative that does support oauth2 could be FairEmail, although it seems like there is some sort of a configuration or possibly even a whitelist issue that's preventing that from working at the moment.  
> Well I could if I wanted maximum security, but in my case I just want an easy access to my email. So if someone steals my bag in a foreign country, I want to be able to take a random computer and connect to my email. And I guess most people don't want to buy a Yubikey just because they want to read their mail on their phone.

Logging in from a random computer is blocked by Google. You either have to have your phone, log in from a known device or from a known Internet connection.

About XOAUTH2 implementation, Thunderbird added it on Desktop in 2016 ([bug 849540](https://bugzilla.mozilla.org/show_bug.cgi?id=849540)). Apart from the ability to detect, remove and renew an outdated XOAUTH2 token (saved as a password), it works fine. [Its implementation code](https://github.com/mozilla/releases-comm-central/search?q=xoauth2) may be useful.
@cketti @daquexian, this issue is pending for years.

It seams that there are ready-to-merge pull requests. So i can't understand why there's no progress on it. 
@sledz App Password works fine for me (using Google Apps, what is usually PITA).
@tbertels Logging in from a random computer is ALLOWED by Google using rescue passwords ... if you have one with you.


@doublehp This has already been discussed in detail, if you scroll up and read through the comments in this issue. The problem is that organizations that use GSuite can choose to enforce 2FA-sans-AppPasswords, and then people are stuck not being able to use K-9 for their work accounts. Also, (I think) Google has a plan to phase it out completely.
Any ETA on the merge?
I also need this feature for an organisation account where I do not have admin rights. 
I have a Microsoft account through school that was working perfectly in K-9 until a couple days ago. IT just updated the requirements.

My work account will follow shortly.

I'll be cut off from the bulk of my correspondences. Anything I can do to help, I'm glad to. I've just joined the beta, too.
I switched to FairEmail for an open source android email client. 

https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq111

From my testing of the three versions (google play / release apk / fdroid) not all can properly support it, but it works for my institution. 
I tested out the pull request and I was unable to add an oauth account for gmail or o365. I left some more stuff in the pull request, thought I'd mention it here, maybe get those issues fixed so we can get this merged atleast. I don't know how one deals with the keys needed for google. I just made my own and that works good for my own release and using personally, but not everyone can compile or wants to. Maybe adding a field to add them and a howto video would be helpful. Be pretty easy to implement too. 
Hello,

It seems that Google will be terminating the traditional authentication method currently in-use by K-9 Mail by May 30.

Here is a mail sent to some accounts with less secure access enabled:

![Google-Auth-EOL](https://user-images.githubusercontent.com/61107724/156539754-cb11d7b0-be0b-4380-83dd-ab025b0b4a4a.png)

This deadline and the popularity of Google accounts in contrast to Yahoo! Mail and US universities' mail systems may bring this issue to a wider attention. At least it did for me.
Note that Google has [app passwords](https://support.google.com/accounts/answer/185833) which is different than the "less-secure apps" that this issue seems to be referring to. Most likely you will still be able to use that, but it might only be enabled if you have 2FA?
The mail suggests that the whole "less secure" access will be disabled, ~~including App Passwords~~.

~~EDIT: Currently, you are required to enable "less secure access" in the Google Account's Settings for K-9 Mail to sign in.~~

~~EDIT2: Less secure access means using your username/password to sign in as well as App Passwords. K-9 Mail uses the former.~~

EDIT3: See below.
I do not have "less secure apps" enabled/disabled (dunno what is the relevant toggle), and instead use app passwords. App passwords is not part of that, and [does not require it to be toggled](https://support.google.com/accounts/answer/185833). [Here](https://support.google.com/accounts/answer/6010255) app password is still listed in "Fix problems".

If you have 2FA, there is no option to toggle "less secure apps", and K-9 works with app passwords.
I see. So this means you will either need 2FA or an OAuth2-compatible application from May 30 onwards.
Arguably, best time to enable those is yesterday. Still K-9 should definitely implement OAuth authentication.

OAuth itself is not complex to implement (you can do basic stuff even with curl), but I don't know what Google's XOAuth and others differ in this sense. From what I've seen in Thunderbird, a Client ID/secret is included in the client itself and hard coded, so I haven't wrapped my head around why that is.
In the past google has referred to user and password credentials as "less secure" regardless if they are App Passwords or not. Their reasoning is that whitelisting apps (at the workspace policy level) and offering scope-based account access is only possible with Oauth2, anything else is considered "less secure".

 https://workspaceupdates.googleblog.com/2019/12/less-secure-apps-oauth-google-username-password-incorrect.html
> It seems that Google will be terminating the traditional authentication method currently in-use by K-9 Mail by May 30.

I also got this information.

After May 30, you can either use OAuth2 or App Passwords. I tested my other programs/apps:
- Thunderbird (PC) supports and uses OAuth2.
- Samsung E-Mail App seems to support OAuth2, because it works if "less secure apps" is disabled.
- K9 does not work if "less secure apps" is disabled.

App passwords require 2FA to be used. 2FA required a phone number added to Google, which I don't have.
You can use "Google Authenthicator App" which is the standard TOTP. I have mine setup to KeepassXC. I don't remember exactly, but you might have to temporarily add a phone to enable the 2FA, but you can decouple it afterwards. There may be quite some fussing around in order to get that disabled, but I know it is possible because one of my accounts has just the "Google Authenthicator" option

Edit: Reactivated 2FA on one account. Indeed you need either a temporary number or security key to initially enroll into 2FA. Do not choose the google prompts, if you want to disable it later on. 
> You can use "Google Authenthicator App" which is the standard TOTP. I have mine setup to KeepassXC. I don't remember exactly, but you might have to temporarily add a phone to enable the 2FA, but you can decouple it afterwards. There may be quite some fussing around in order to get that disabled, but I know it is possible because one of my accounts has just the "Google Authenthicator" option
> 
> Edit: Reactivated 2FA on one account. Indeed you need either a temporary number or security key to initially enroll into 2FA. Do not choose the google prompts, if you want to disable it later on.

There is another way without being forced to add a phone number. Use a Yubikey or similar U2F. And if you don't have one, you can temporarily emulate one in software with e.g. https://github.com/danstiner/rust-u2f. Just make sure you switch to OTP afterwards as emulating one does not increase security over your password.
The email I got from Google did not say anything about App Passwords continuing to work after May 30th, it just said to use "other more secure technologies, like OAuth 2.0" with a ["Learn more" link](https://support.google.com/accounts/answer/6010255) but that page was not very clear about which of the things it mentions will or won't still be available after May 30th.

Even more confusingly, the [app passwords help page](https://support.google.com/mail/answer/185833?hl=en-GB) says "You may not be able to use an app password for less secure apps".  Now I thought an "app password" was a backward-compatibility option, and "less secure apps" is also their term for apps that need a backward compatibility option, so that sentence seems to be saying "you might not be able to use this thing on apps that need this thing".

So I wouldn't like to depend on "App Passwords" still being available, unless there is some official text I haven't seen that *explicitly* says these will still be available after May 30.

Right now the only open standard we *know* they still intend to support after May 30 is OAuth2.
> You can use "Google Authenthicator App" which is the standard TOTP. I have mine setup to KeepassXC. I don't remember exactly, but you might have to temporarily add a phone to enable the 2FA, but you can decouple it afterwards. There may be quite some fussing around in order to get that disabled, but I know it is possible because one of my accounts has just the "Google Authenthicator" option
> 
> Edit: Reactivated 2FA on one account. Indeed you need either a temporary number or security key to initially enroll into 2FA. Do not choose the google prompts, if you want to disable it later on.

Thanks. It worked. I was able to enable 2FA and generated app passwords for both of my accounts in K9.

Enabling 2FA was somehow strange:
- Account 1: Google forced me using a screen-lock, which I already had. I added the default confirmation on the smartphone, my phone-number, the backup-list of codes and the Google Authenticator. All of them works.
- Account 2: No phone-number, no screen-look, because it is only used for games. Google didn't care about the missing screen-look. I just added the default confirmation, the backup-list of codes and the Google Authenticator. It also works. So you don't need phone-number at all.
...but we still don't know for sure if App Passwords will continue to work after May 30 (as I said above).

Further, App password are only available if the organization allows it. There are schools that do not. 

On March 4, 2022 6:54:57 AM EST, "Silas S. Brown" ***@***.***> wrote:
>...but we still don't know for sure if App Passwords will continue to work after May 30 (as I said above).
>
>
>-- 
>Reply to this email directly or view it on GitHub:
>https://github.com/k9mail/k-9/issues/655#issuecomment-1059097474
>You are receiving this because you commented.
>
>Message ID: ***@***.***>
I just set up oauth2 authentification for offlineimap.
This wasn't very smooth but quite easy. You need to do this with a browser but no 2FA nor phone. 
Please let us use that. I will not activate 2FA and hope to get soon off the evil G boat.

Here is the link I followed :
https://hobo.house/2017/07/17/using-offlineimap-with-the-gmail-imap-api/
I have switched phone OSes twice now since this issue was opened seven years ago about core compatibility with the world's largest email provider. I'm pretty sure it's never getting added, lol.
@cketti 
If nothing is done, the userbase of K9mail will diminish fast in less than 7 weeks. I must admit that I will go and only will look back when I would have got rid of Gmail completly. Too bad but not so soon...
I do not know anything to help coding but found some path that I used with offlineimap that might help. Here is it.
```
## Enable oauth2 for gmail

# make new project here
https://console.developers.google.com/apis/api/gmail.googleapis.com/overview

# create credentials, ID client oAuth2
# configure oAuth validation screen for test app
# only name off app and twice your email
# validate full access
# add yourself to allowed to test group

# Back to credential screen
# Choose desktop app
# generate ID and secret
# register to pass account_ID and account_SEC

# generate refresh token with oauth.py from https://github.com/sadsfae/misc-dotfiles
python2 oauth2.py --generate_oauth2_token --client_id=YOUR_CLIENT_ID --client_secret=YOUR_CLIENT_SECRET
python2 oauth2.py --generate_oauth2_token --client_id=$(pass account_ID) --client_secret=$(pass account_SEC)

# use browser to get the key that will validate the query

# copy refresh token or/and register to pass account_RTOKEN

# configure offlineimap in offlineimaprc
[Repository account_Remote]
type = IMAP
remotehost = imap.gmail.com
ssl = yes
starttls = no
ssl_version = tls1_2
remoteuser = account@gmail.com
auth_mechanisms   = XOAUTH2
oauth2_client_id =
oauth2_client_secret =
oauth2_request_url = https://accounts.google.com/o/oauth2/token
oauth2_refresh_token =
realdelete = yes
createfolders= False
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
folderfilter = (1)
nametrans = (2)	(3)

source : https://hobo.house/2017/07/17/using-offlineimap-with-the-gmail-imap-api/
Posted on July 17, 2017 by Will Foster
```
Even if no one who is working on K-9 cares about GMail, many Americans may use it for the miscellaneous and spam and sign-up folder. I am tech savvy for the most part. I keep a paid-for separate e-mail account, a work account, and I use my GMail account for necessary sign-ups for Google services or for some website to buy stuff. Once that vendor has my e-mail address, I get inundated. Nothing of life or death importance ever goes to my GMail. I never tell people in real life what my GMail address is.

Many Americans and people elsewhere, however, use GMail for everything or at least for some essential tasks. I haven't yet recommended K-9 Mail to anyone because of that. Even when one could go and enable "less secure apps," I knew that 99.9% of everyone in America at least would be unable to understand the problem, let alone fix it.

This is to say nothing of the fact that other e-mail providers have or will soon switch to OAuth-only. Microsoft and several universities, for example, have been itching to switch over, but the pandemic delayed plans.

I do know that every site's implementation is different, and so implementation and testing each will be onerous, but having even one implementation function is a huge step.

This feature in the K-9 program would enable new users to adopt K-9. Some of them will be talented enough to contribute code. If they don't ever adopt K-9, however, they will be unlikely to contribute code.
Hi,
I can second that.  I definitely do see the shift to OAuth, where password-based IMAP gets just switched off.  University Microsoft-hosted email just switched to OAuth only here.

I'm unfortunately not an app developer and cannot contribute the implementation here - but I think it as crucial to K-9 Mail staying relevant.

Otherwise it might just get labeled as "does not work with a number of providers".

Cheers,
Vlad

PS: For GMail specifically, it may still work for users with 2FA ... but that won't stop online rumours from labeling K-9 as "not working with GMail"....
Is there any movement on this at all or do we all (k9 fans of course) have to watch out for another mail client? Would be great to know in which direction thid topic moves. 
@Maetts Not a ton. I tried the PR  and it didn't work.
I would support - if I could code. Checked already some alternative apps but there is literally nothing like k9. pEp mail is basically a clone of k9 but with oauth support.
Like already said in this thread, Fairemail supports Gmail's OAuth, though only for on-device accounts and with the Google Play version.
https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq111

> non on-device accounts is not supported because Google requires a [yearly security audit](https://support.google.com/cloud/answer/9110914) ($15,000 to $75,000) for this.
Yeah thanks for the hint, I tested fairemail but didn't like it. Not like I love k9 since decades and on manymanymany phones.

@cketti is there really nothing new on this topic? Just to know if we have to arrange with another mail client.. 😔
I have almost daily alerts from Gmail that my password was used from uncommon place... and no sync if I don't go and check their box.
I will not activate 2 factor auth.  Really don't know what I will do.
Would be nice to know if @cketti is working on it at all.
take a look onto
https://developers.google.com/identity/protocols/oauth2/native-app
there is link to Standart library ... possible that simple may be linked into app ...
https://github.com/openid/AppAuth-Android
that is even made without dependency on webkit by deescription
Is really nothing moving on this here? 😔
I can't also access to my university email because it use oauth2 trought outlook
It seems Gmail is about to remove "[less secure apps](https://support.google.com/accounts/answer/6010255)" access, with Google Workspace to follow:

> To help keep your account secure, starting May 30, 2022, ​​Google will no longer support the use of third-party apps or devices which ask you to sign in to your Google Account using only your username and password.
While it will be good to have OAuth2 support, the key part in that announcement "Google Account using only your username and password", is that what is being disabled is using your google (sitewide) pw for login via 3rd-party apps. K-9 will continue to work with gmail if ... you set up, and configure K-9 to use, google "app passwords":

<https://support.google.com/accounts/answer/6010255#zippy=%2Cuse-an-app-password>

My sense is that MS Exchange may be disabling app passwords soon (including removing the ability for administrators to control whether they are accepted or not), but it should be noted that some bits of their implementation of OAuth2 differ from what is needed for gmail. 

> While it will be good to have OAuth2 support, the key part in that announcement "Google Account using only your username and password", is that what is being disabled is using your google (sitewide) pw for login via 3rd-party apps. K-9 will continue to work with gmail if ... you set up, and configure K-9 to use, google "app passwords":

While this is true for most personal accounts, Google does not allow the creation of app passwords for organization (school, work) accounts. I mostly use K9 with my school email address, so it looks like on may 30 I will be unable to use K9 anymore. The fact that you can't create app passwords for organization accounts is documented here:

https://support.google.com/accounts/answer/185833

Indeed, there is no button for me to create an app password.

So, unlike the responses to the many issues/forum posts panicking about this would suggest, I believe that this will in fact completely break support for many users, myself included. 
Delta Chat on F-Droid handles OAuth2. It's not a practical email client, but it shows how can be implemented.

Without OAuth2 support, I'm unable to use K-9 anymore. It's not a matter of choice, it's sheer necessity.
Another FOSS app that offers authentication is [FairEmail](https://email.faircode.eu/). From the sign-in process it looks like they leverage logged-in accounts on the device–as I got a microG confirmation dialogue before being able to log in; I think this idea was floated up before in this thread, maybe their implementation can be a good starting point to do the same thing in K9?
> While this is true for most personal accounts, Google does not allow the creation of app passwords for organization (school, work) accounts. I mostly use K9 with my school email address, so it looks like on may 30 I will be unable to use K9 anymore. The fact that you can't create app passwords for organization accounts is documented here:
> 
> https://support.google.com/accounts/answer/185833
> 
> Indeed, there is no button for me to create an app password.

Hi,

I think the meaning is slightly different: I think for organization accounts, application passwords *can* be centrally disabled or left enabled.  I'm using application passwords on my organization-managed gmail account.  So likely driven by a central decision made by the organization's administrator.

But that unfortunately still does not help those users where the administrator decided to block application passwords...

Cheers,
Vlad
Hi,

while K9 is an open source project I (we) cand demand someone to add any feature, including OAUTH!
The "demand" for OAUTH is around for years, first posting in this thread is from 2015. For me it would be fine to hear from the dev-team if it is pland to implement OAUTH or not. I'm not asking when! I think it's not helping anyone if we all post here "provider XYZ is switching to OAUTH", that's a fact everybody is aware (I think).

Yours
rol1
My experience with Google is that app passwords can be created once 2FA is enabled.
My experience with Microsoft 365 is that an admin needs to allow app passwords for the whole organization [here](https://account.activedirectory.windowsazure.com/UserManagement/MfaSettings.aspx), and also enable Multi Factor Auth for the specific user [here](https://account.activedirectory.windowsazure.com/usermanagement/multifactorverification.aspx). After these two steps, users can create app passwords [here](https://mysignins.microsoft.com/security-info), but they won't work anyway for imap access. I have a long-standing ticket with Microsoft support but so far they've only been able to ask why on earth would I not use "modern authentication" like everyone else, while at the same time requiring me to send them loads of Outlook (!) logs and screenshots.
> I think it's not helping anyone if we all post here "provider XYZ is switching to OAUTH", that's a fact everybody is aware (I think).

Well, not everyone gets these announcements or has the time to interpret them and evaluate their impact. The issue here is that app passwords are supposed to be our lifeline to continue using K9-Mail (which is what we all want to do), and I for one feel that lifeline becoming increasingly thin and threatened. In my experience they work flawlessly with Yahoo, come with some caveats for Google, and are completely broken on Microsoft, just to cite a handful of major providers. What do you do if your employer uses Microsoft? Hopefully you check out if anything is moving forward with your favourite email app before switching to Spark or Bluemail for good.
> ... What do you do if your employer uses Microsoft? Hopefully you check out if anything is moving forward with your favourite email app before switching to Spark or Bluemail for good.

That's the reason why I asked if there is a plan to implement OAUTH or not. If not, I would swicht, if it  will be implemented my next question would be when we could expect it, but that's question no 2!
Can someone lock this ticket to further commenting? This is an issue tracker not a discussion forum. Some of us have notifications enabled on this issue so we can know when progress is made toward resolving it. Seeing the same theories and speculations that have been posted regularly for the seven years this issue has been open is of no use to anyone. There is an official forum that is far better suited for redundant pondering: https://forum.k9mail.app/
"Delta Lab" can log in simultaneously into multiple oauth accounts. It doesn't need Google Services and it doesn't require any android system account.

However, since it's a chat app, it's not a viable email client. It doesn't have unified inbox and, before you can send a message to someone, you must have him in your contact list.

Nevertheless, it's open source software and the way it handles oauth authentication could be of some interest to k-9 developers, in case they wanted to implement it.

https://github.com/adbenitez/deltalab-android/releases/tag/v1.30.0-deltalab
Hello. Does the future plan for K-9 Mail include OAUTH support?

After reading this thread and others, I'm unable to tell whether this is currently in the works or whether it is considered "not necessary".

I would like to know the status of this so that I can begin my migration away from K-9 Mail as soon as possible if necessary.
> Hello. Does the future plan for K-9 Mail include OAUTH support?
> 
> 
> 
> After reading this thread and others, I'm unable to tell whether this is currently in the works or whether it is considered "not necessary".
> 
> 
> 
> I would like to know the status of this so that I can begin my migration away from K-9 Mail as soon as possible if necessary.

This issue has been open for seven years. I've switched phone OSes twice since I first commented.

...It's not gonna happen.
Looks like FairEmail is dead:

> All my projects have been terminated after Google falsely flagged FairEmail as spyware without a reasonable opportunity to appeal. There will be no further development and no more support.

Source: https://email.faircode.eu

We really need this in K-9 😢 
> Looks like FairEmail is dead:

FairEmail is open source https://github.com/M66B/FairEmail and available on F-Droid. A fork is pretty likely.
Support for this feature is planned for the next release. See https://forum.k9mail.app/t/the-plan-for-k-9-mail-6-200/4590

Please don't use this issue tracker as a discussion forum. Use the actual forum instead: https://forum.k9mail.app/

Pull request: Add support for OAuth 2.0 (Gmail)
The implementation cuts a few corners. The plan is to clean this up when we rework the account setup code to support Thunderbird's Autoconfiguration (#865).

Fixes #655

Test command: ./gradlew :mail:protocols:smtp:test --tests "com.fsck.k9.mail.transport.smtp.SmtpTransportTest" :mail:protocols:imap:test --tests "com.fsck.k9.mail.store.imap.RealImapConnectionTest"

Fix patch:
diff --git a/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt b/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt
index ef7651fe10b..0992656beb4 100644
--- a/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt
+++ b/app/autodiscovery/providersxml/src/main/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscovery.kt
@@ -104,7 +104,14 @@ class ProvidersXmlDiscovery(
             uri.port
         }
 
-        return DiscoveredServerSettings(Protocols.IMAP, host, port, security, AuthType.PLAIN, username)
+        // TODO: Remove this hack
+        val authType = if (host == "imap.gmail.com" || host == "imap.googlemail.com") {
+            AuthType.XOAUTH2
+        } else {
+            AuthType.PLAIN
+        }
+
+        return DiscoveredServerSettings(Protocols.IMAP, host, port, security, authType, username)
     }
 
     private fun Provider.toOutgoingServerSettings(email: String): DiscoveredServerSettings? {
@@ -127,7 +134,14 @@ class ProvidersXmlDiscovery(
             uri.port
         }
 
-        return DiscoveredServerSettings(Protocols.SMTP, host, port, security, AuthType.PLAIN, username)
+        // TODO: Remove this hack
+        val authType = if (host == "smtp.gmail.com" || host == "smtp.googlemail.com") {
+            AuthType.XOAUTH2
+        } else {
+            AuthType.PLAIN
+        }
+
+        return DiscoveredServerSettings(Protocols.SMTP, host, port, security, authType, username)
     }
 
     private fun String.fillInUsernameTemplate(email: String, user: String, domain: String): String {
diff --git a/app/core/src/main/java/com/fsck/k9/Account.kt b/app/core/src/main/java/com/fsck/k9/Account.kt
index 2195e1fcebc..454007a79d3 100644
--- a/app/core/src/main/java/com/fsck/k9/Account.kt
+++ b/app/core/src/main/java/com/fsck/k9/Account.kt
@@ -34,6 +34,10 @@ class Account(override val uuid: String) : BaseAccount {
             internalOutgoingServerSettings = value
         }
 
+    @get:Synchronized
+    @set:Synchronized
+    var oAuthState: String? = null
+
     /**
      * Storage provider ID, used to locate and manage the underlying DB/file storage.
      */
diff --git a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt
index 3bb834d7b12..00bafbdb70d 100644
--- a/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt
+++ b/app/core/src/main/java/com/fsck/k9/AccountPreferenceSerializer.kt
@@ -36,6 +36,7 @@ class AccountPreferenceSerializer(
             outgoingServerSettings = serverSettingsSerializer.deserialize(
                 storage.getString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", "")
             )
+            oAuthState = storage.getString("$accountUuid.oAuthState", null)
             localStorageProviderId = storage.getString("$accountUuid.localStorageProvider", storageManager.defaultProviderId)
             name = storage.getString("$accountUuid.description", null)
             alwaysBcc = storage.getString("$accountUuid.alwaysBcc", alwaysBcc)
@@ -240,6 +241,7 @@ class AccountPreferenceSerializer(
         with(account) {
             editor.putString("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(incomingServerSettings))
             editor.putString("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY", serverSettingsSerializer.serialize(outgoingServerSettings))
+            editor.putString("$accountUuid.oAuthState", oAuthState)
             editor.putString("$accountUuid.localStorageProvider", localStorageProviderId)
             editor.putString("$accountUuid.description", name)
             editor.putString("$accountUuid.alwaysBcc", alwaysBcc)
@@ -359,6 +361,7 @@ class AccountPreferenceSerializer(
 
         editor.remove("$accountUuid.$INCOMING_SERVER_SETTINGS_KEY")
         editor.remove("$accountUuid.$OUTGOING_SERVER_SETTINGS_KEY")
+        editor.remove("$accountUuid.oAuthState")
         editor.remove("$accountUuid.description")
         editor.remove("$accountUuid.name")
         editor.remove("$accountUuid.email")
diff --git a/app/core/src/main/java/com/fsck/k9/Preferences.kt b/app/core/src/main/java/com/fsck/k9/Preferences.kt
index f7eaab83d92..eca0f09fb1a 100644
--- a/app/core/src/main/java/com/fsck/k9/Preferences.kt
+++ b/app/core/src/main/java/com/fsck/k9/Preferences.kt
@@ -201,7 +201,7 @@ class Preferences internal constructor(
     val defaultAccount: Account?
         get() = accounts.firstOrNull()
 
-    fun saveAccount(account: Account) {
+    override fun saveAccount(account: Account) {
         ensureAssignedAccountNumber(account)
         processChangedValues(account)
 
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt b/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt
index 5ae56c7e1d2..4037932c770 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt
+++ b/app/core/src/main/java/com/fsck/k9/preferences/AccountManager.kt
@@ -13,4 +13,5 @@ interface AccountManager {
     fun moveAccount(account: Account, newPosition: Int)
     fun addOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener)
     fun removeOnAccountsChangeListener(accountsChangeListener: AccountsChangeListener)
+    fun saveAccount(account: Account)
 }
diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle
index aae39b45b16..80c8b92a510 100644
--- a/app/k9mail/build.gradle
+++ b/app/k9mail/build.gradle
@@ -65,6 +65,12 @@ android {
 
     signingConfigs {
         release
+        debug {
+            keyAlias = "androiddebugkey"
+            keyPassword = "android"
+            storeFile = file("$rootProject.projectDir/debug.keystore")
+            storePassword = "android"
+        }
     }
 
     buildTypes {
@@ -76,16 +82,22 @@ android {
             minifyEnabled true
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 
-            buildConfigField "boolean", "DEVELOPER_MODE", "false"
+            buildConfigField "String", "OAUTH_GMAIL_CLIENT_ID", "\"262622259280-hhmh92rhklkg2k1tjil69epo0o9a12jm.apps.googleusercontent.com\""
+
+            manifestPlaceholders = ['appAuthRedirectScheme': 'com.fsck.k9']
         }
 
         debug {
             applicationIdSuffix ".debug"
             testCoverageEnabled rootProject.testCoverage
 
+            signingConfig signingConfigs.debug
+
             minifyEnabled false
 
-            buildConfigField "boolean", "DEVELOPER_MODE", "true"
+            buildConfigField "String", "OAUTH_GMAIL_CLIENT_ID", "\"262622259280-5qb3vtj68d5dtudmaif4g9vd3cpar8r3.apps.googleusercontent.com\""
+
+            manifestPlaceholders = ['appAuthRedirectScheme': 'com.fsck.k9.debug']
         }
     }
 
diff --git a/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt b/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt
index ff39743fcd3..f319af92529 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/Dependencies.kt
@@ -1,5 +1,7 @@
 package com.fsck.k9
 
+import com.fsck.k9.activity.setup.OAuthCredentials
+import com.fsck.k9.auth.K9OAuthCredentials
 import com.fsck.k9.backends.backendsModule
 import com.fsck.k9.controller.ControllerExtension
 import com.fsck.k9.crypto.EncryptionExtractor
@@ -29,6 +31,7 @@ private val mainAppModule = module {
     single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
     single<EncryptionExtractor> { OpenPgpEncryptionExtractor.newInstance() }
     single<StoragePersister> { K9StoragePersister(get()) }
+    factory<OAuthCredentials> { K9OAuthCredentials() }
 }
 
 val appModules = listOf(
diff --git a/app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt b/app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt
new file mode 100644
index 00000000000..281cfcc4ee1
--- /dev/null
+++ b/app/k9mail/src/main/java/com/fsck/k9/auth/K9OAuthCredentials.kt
@@ -0,0 +1,9 @@
+package com.fsck.k9.auth
+
+import com.fsck.k9.BuildConfig
+import com.fsck.k9.activity.setup.OAuthCredentials
+
+class K9OAuthCredentials : OAuthCredentials {
+    override val gmailClientId: String
+        get() = BuildConfig.OAUTH_GMAIL_CLIENT_ID
+}
diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt
index 989f4286559..2b14b1b0ffd 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/backends/ImapBackendFactory.kt
@@ -1,11 +1,12 @@
 package com.fsck.k9.backends
 
+import android.content.Context
 import com.fsck.k9.Account
 import com.fsck.k9.backend.BackendFactory
 import com.fsck.k9.backend.api.Backend
 import com.fsck.k9.backend.imap.ImapBackend
 import com.fsck.k9.backend.imap.ImapPushConfigProvider
-import com.fsck.k9.mail.oauth.OAuth2TokenProvider
+import com.fsck.k9.mail.AuthType
 import com.fsck.k9.mail.power.PowerManager
 import com.fsck.k9.mail.ssl.TrustedSocketFactory
 import com.fsck.k9.mail.store.imap.IdleRefreshManager
@@ -23,7 +24,8 @@ class ImapBackendFactory(
     private val powerManager: PowerManager,
     private val idleRefreshManager: IdleRefreshManager,
     private val backendStorageFactory: K9BackendStorageFactory,
-    private val trustedSocketFactory: TrustedSocketFactory
+    private val trustedSocketFactory: TrustedSocketFactory,
+    private val context: Context
 ) : BackendFactory {
     override fun createBackend(account: Account): Backend {
         val accountName = account.displayName
@@ -44,7 +46,12 @@ class ImapBackendFactory(
     }
 
     private fun createImapStore(account: Account): ImapStore {
-        val oAuth2TokenProvider: OAuth2TokenProvider? = null
+        val oAuth2TokenProvider = if (account.incomingServerSettings.authenticationType == AuthType.XOAUTH2) {
+            RealOAuth2TokenProvider(context, accountManager, account)
+        } else {
+            null
+        }
+
         val config = createImapStoreConfig(account)
         return ImapStore.create(
             account.incomingServerSettings,
@@ -67,7 +74,12 @@ class ImapBackendFactory(
 
     private fun createSmtpTransport(account: Account): SmtpTransport {
         val serverSettings = account.outgoingServerSettings
-        val oauth2TokenProvider: OAuth2TokenProvider? = null
+        val oauth2TokenProvider = if (serverSettings.authenticationType == AuthType.XOAUTH2) {
+            RealOAuth2TokenProvider(context, accountManager, account)
+        } else {
+            null
+        }
+
         return SmtpTransport(serverSettings, trustedSocketFactory, oauth2TokenProvider)
     }
 
diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
index 5585a2bdcc1..b23177632c1 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/backends/KoinModule.kt
@@ -26,7 +26,8 @@ val backendsModule = module {
             powerManager = get(),
             idleRefreshManager = get(),
             backendStorageFactory = get(),
-            trustedSocketFactory = get()
+            trustedSocketFactory = get(),
+            context = get()
         )
     }
     single<SystemAlarmManager> { AndroidAlarmManager(context = get(), alarmManager = get()) }
diff --git a/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt
new file mode 100644
index 00000000000..a2828972064
--- /dev/null
+++ b/app/k9mail/src/main/java/com/fsck/k9/backends/RealOAuth2TokenProvider.kt
@@ -0,0 +1,65 @@
+package com.fsck.k9.backends
+
+import android.content.Context
+import com.fsck.k9.Account
+import com.fsck.k9.mail.AuthenticationFailedException
+import com.fsck.k9.mail.oauth.OAuth2TokenProvider
+import com.fsck.k9.preferences.AccountManager
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import net.openid.appauth.AuthState
+import net.openid.appauth.AuthorizationException
+import net.openid.appauth.AuthorizationService
+
+class RealOAuth2TokenProvider(
+    context: Context,
+    private val accountManager: AccountManager,
+    private val account: Account
+) : OAuth2TokenProvider {
+    private val authService = AuthorizationService(context)
+    private var requestFreshToken = false
+
+    override fun getToken(timeoutMillis: Long): String {
+        val latch = CountDownLatch(1)
+        var token: String? = null
+        var exception: AuthorizationException? = null
+
+        val authState = account.oAuthState?.let { AuthState.jsonDeserialize(it) }
+            ?: throw AuthenticationFailedException("Login required")
+
+        if (requestFreshToken) {
+            authState.needsTokenRefresh = true
+        }
+
+        val oldAccessToken = authState.accessToken
+
+        authState.performActionWithFreshTokens(authService) { accessToken: String?, _, authException: AuthorizationException? ->
+            token = accessToken
+            exception = authException
+
+            latch.countDown()
+        }
+
+        latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
+
+        if (exception != null || token != oldAccessToken) {
+            requestFreshToken = false
+            account.oAuthState = authState.jsonSerializeString()
+            accountManager.saveAccount(account)
+        }
+
+        exception?.let { authException ->
+            throw AuthenticationFailedException(
+                message = "Failed to fetch an access token",
+                throwable = authException,
+                messageFromServer = authException.error
+            )
+        }
+
+        return token ?: throw AuthenticationFailedException("Failed to fetch an access token")
+    }
+
+    override fun invalidateToken() {
+        requestFreshToken = true
+    }
+}
diff --git a/app/ui/legacy/build.gradle b/app/ui/legacy/build.gradle
index d7d82b89dcc..ee479bd31b3 100644
--- a/app/ui/legacy/build.gradle
+++ b/app/ui/legacy/build.gradle
@@ -40,6 +40,7 @@ dependencies {
     implementation "com.mikepenz:fastadapter-extensions-drag:${versions.fastAdapter}"
     implementation "com.mikepenz:fastadapter-extensions-utils:${versions.fastAdapter}"
     implementation 'de.hdodenhof:circleimageview:3.1.0'
+    api 'net.openid:appauth:0.11.1'
 
     implementation "commons-io:commons-io:${versions.commonsIo}"
     implementation "androidx.core:core-ktx:${versions.androidxCore}"
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt
index 93734a00613..f25afc496c2 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/KoinModule.kt
@@ -1,7 +1,10 @@
 package com.fsck.k9.activity
 
+import com.fsck.k9.activity.setup.AuthViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
 import org.koin.dsl.module
 
 val activityModule = module {
-    single { MessageLoaderHelperFactory(get(), get()) }
+    single { MessageLoaderHelperFactory(messageViewInfoExtractorFactory = get(), htmlSettingsProvider = get()) }
+    viewModel { AuthViewModel(application = get(), accountManager = get(), oauthCredentials = get()) }
 }
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt
index 41d2c0df8dd..d3c6c4caefe 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupBasics.kt
@@ -4,6 +4,7 @@ import android.content.Context
 import android.content.Intent
 import android.os.Bundle
 import android.text.Editable
+import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
 import android.widget.CheckBox
@@ -25,6 +26,8 @@ import com.fsck.k9.mailstore.SpecialLocalFoldersCreator
 import com.fsck.k9.ui.ConnectionSettings
 import com.fsck.k9.ui.R
 import com.fsck.k9.ui.base.K9Activity
+import com.fsck.k9.ui.getEnum
+import com.fsck.k9.ui.putEnum
 import com.fsck.k9.ui.settings.ExtraAccountDiscovery
 import com.fsck.k9.view.ClientCertificateSpinner
 import com.google.android.material.textfield.TextInputEditText
@@ -46,12 +49,15 @@ class AccountSetupBasics : K9Activity() {
 
     private lateinit var emailView: TextInputEditText
     private lateinit var passwordView: TextInputEditText
+    private lateinit var passwordLayout: View
     private lateinit var clientCertificateCheckBox: CheckBox
     private lateinit var clientCertificateSpinner: ClientCertificateSpinner
+    private lateinit var advancedOptionsContainer: View
     private lateinit var nextButton: Button
     private lateinit var manualSetupButton: Button
     private lateinit var allowClientCertificateView: ViewGroup
 
+    private var uiState = UiState.EMAIL_ADDRESS_ONLY
     private var account: Account? = null
     private var checkedIncoming = false
 
@@ -62,14 +68,15 @@ class AccountSetupBasics : K9Activity() {
 
         emailView = findViewById(R.id.account_email)
         passwordView = findViewById(R.id.account_password)
+        passwordLayout = findViewById(R.id.account_password_layout)
         clientCertificateCheckBox = findViewById(R.id.account_client_certificate)
         clientCertificateSpinner = findViewById(R.id.account_client_certificate_spinner)
         allowClientCertificateView = findViewById(R.id.account_allow_client_certificate)
+        advancedOptionsContainer = findViewById(R.id.foldable_advanced_options)
         nextButton = findViewById(R.id.next)
         manualSetupButton = findViewById(R.id.manual_setup)
 
         manualSetupButton.setOnClickListener { onManualSetup() }
-        nextButton.setOnClickListener { onNext() }
     }
 
     override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -82,12 +89,15 @@ class AccountSetupBasics : K9Activity() {
          */
         initializeViewListeners()
         validateFields()
+
+        updateUi()
     }
 
     private fun initializeViewListeners() {
         val textWatcher = object : SimpleTextWatcher() {
             override fun afterTextChanged(s: Editable) {
-                validateFields()
+                val checkPassword = uiState == UiState.PASSWORD_FLOW
+                validateFields(checkPassword)
             }
         }
 
@@ -109,9 +119,25 @@ class AccountSetupBasics : K9Activity() {
         }
     }
 
+    private fun updateUi() {
+        when (uiState) {
+            UiState.EMAIL_ADDRESS_ONLY -> {
+                passwordLayout.isVisible = false
+                advancedOptionsContainer.isVisible = false
+                nextButton.setOnClickListener { attemptAutoSetupUsingOnlyEmailAddress() }
+            }
+            UiState.PASSWORD_FLOW -> {
+                passwordLayout.isVisible = true
+                advancedOptionsContainer.isVisible = true
+                nextButton.setOnClickListener { attemptAutoSetup() }
+            }
+        }
+    }
+
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
 
+        outState.putEnum(STATE_KEY_UI_STATE, uiState)
         outState.putString(EXTRA_ACCOUNT, account?.uuid)
         outState.putBoolean(STATE_KEY_CHECKED_INCOMING, checkedIncoming)
     }
@@ -119,6 +145,8 @@ class AccountSetupBasics : K9Activity() {
     override fun onRestoreInstanceState(savedInstanceState: Bundle) {
         super.onRestoreInstanceState(savedInstanceState)
 
+        uiState = savedInstanceState.getEnum(STATE_KEY_UI_STATE, UiState.EMAIL_ADDRESS_ONLY)
+
         val accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT)
         if (accountUuid != null) {
             account = preferences.getAccount(accountUuid)
@@ -132,9 +160,10 @@ class AccountSetupBasics : K9Activity() {
         allowClientCertificateView.isVisible = usingCertificates
     }
 
-    private fun validateFields() {
+    private fun validateFields(checkPassword: Boolean = true) {
         val email = emailView.text?.toString().orEmpty()
-        val valid = requiredFieldValid(emailView) && emailValidator.isValidAddressOnly(email) && isPasswordFieldValid()
+        val valid = requiredFieldValid(emailView) && emailValidator.isValidAddressOnly(email) &&
+            (!checkPassword || isPasswordFieldValid())
 
         nextButton.isEnabled = valid
         nextButton.isFocusable = valid
@@ -149,7 +178,37 @@ class AccountSetupBasics : K9Activity() {
             clientCertificateChecked && clientCertificateAlias != null
     }
 
-    private fun onNext() {
+    private fun attemptAutoSetupUsingOnlyEmailAddress() {
+        val email = emailView.text?.toString() ?: error("Email missing")
+
+        val extraConnectionSettings = ExtraAccountDiscovery.discover(email)
+        if (extraConnectionSettings != null) {
+            finishAutoSetup(extraConnectionSettings)
+            return
+        }
+
+        val connectionSettings = providersXmlDiscoveryDiscover(email)
+
+        if (connectionSettings != null &&
+            connectionSettings.incoming.authenticationType == AuthType.XOAUTH2 &&
+            connectionSettings.outgoing.authenticationType == AuthType.XOAUTH2
+        ) {
+            finishAutoSetup(connectionSettings)
+        } else {
+            startPasswordFlow()
+        }
+    }
+
+    private fun startPasswordFlow() {
+        uiState = UiState.PASSWORD_FLOW
+
+        updateUi()
+        validateFields()
+
+        passwordView.requestFocus()
+    }
+
+    private fun attemptAutoSetup() {
         if (clientCertificateCheckBox.isChecked) {
             // Auto-setup doesn't support client certificates.
             onManualSetup()
@@ -272,8 +331,14 @@ class AccountSetupBasics : K9Activity() {
         }
     }
 
+    private enum class UiState {
+        EMAIL_ADDRESS_ONLY,
+        PASSWORD_FLOW
+    }
+
     companion object {
         private const val EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account"
+        private const val STATE_KEY_UI_STATE = "com.fsck.k9.AccountSetupBasics.uiState"
         private const val STATE_KEY_CHECKED_INCOMING = "com.fsck.k9.AccountSetupBasics.checkedIncoming"
 
         @JvmStatic
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt
index dd2b71c4de0..c6e88d17962 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.kt
@@ -18,6 +18,7 @@ import com.fsck.k9.Preferences
 import com.fsck.k9.controller.MessagingController
 import com.fsck.k9.fragment.ConfirmationDialogFragment
 import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
+import com.fsck.k9.mail.AuthType
 import com.fsck.k9.mail.AuthenticationFailedException
 import com.fsck.k9.mail.CertificateValidationException
 import com.fsck.k9.mail.MailServerDirection
@@ -25,13 +26,16 @@ import com.fsck.k9.mail.filter.Hex
 import com.fsck.k9.preferences.Protocols
 import com.fsck.k9.ui.R
 import com.fsck.k9.ui.base.K9Activity
+import com.fsck.k9.ui.observe
 import java.security.MessageDigest
 import java.security.NoSuchAlgorithmException
 import java.security.cert.CertificateEncodingException
 import java.security.cert.CertificateException
 import java.security.cert.X509Certificate
 import java.util.Locale
+import java.util.concurrent.Executors
 import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
 import timber.log.Timber
 
 /**
@@ -41,6 +45,8 @@ import timber.log.Timber
  * while its thread is running.
  */
 class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListener {
+    private val authViewModel: AuthViewModel by viewModel()
+
     private val messagingController: MessagingController by inject()
     private val preferences: Preferences by inject()
     private val localKeyStoreManager: LocalKeyStoreManager by inject()
@@ -63,6 +69,33 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen
         super.onCreate(savedInstanceState)
         setLayout(R.layout.account_setup_check_settings)
 
+        authViewModel.init(activityResultRegistry, lifecycle)
+
+        authViewModel.uiState.observe(this) { state ->
+            when (state) {
+                AuthFlowState.Idle -> {
+                    return@observe
+                }
+                AuthFlowState.Success -> {
+                    startCheckServerSettings()
+                }
+                AuthFlowState.Canceled -> {
+                    showErrorDialog(R.string.account_setup_failed_dlg_oauth_flow_canceled)
+                }
+                is AuthFlowState.Failed -> {
+                    showErrorDialog(R.string.account_setup_failed_dlg_oauth_flow_failed, state)
+                }
+                AuthFlowState.NotSupported -> {
+                    showErrorDialog(R.string.account_setup_failed_dlg_oauth_not_supported)
+                }
+                AuthFlowState.BrowserNotFound -> {
+                    showErrorDialog(R.string.account_setup_failed_dlg_browser_not_found)
+                }
+            }
+
+            authViewModel.authResultConsumed()
+        }
+
         messageView = findViewById(R.id.message)
         progressBar = findViewById(R.id.progress)
         findViewById<View>(R.id.cancel).setOnClickListener { onCancel() }
@@ -75,7 +108,26 @@ class AccountSetupCheckSettings : K9Activity(), ConfirmationDialogFragmentListen
         direction = intent.getSerializableExtra(EXTRA_CHECK_DIRECTION) as CheckDirection?
             ?: error("Missing CheckDirection")
 
-        CheckAccountTask(account).execute(direction)
+        if (savedInstanceState == null) {
+            if (needsAuthorization()) {
+                setMessage(R.string.account_setup_check_settings_authenticate)
+                authViewModel.login(account)
+            } else {
+                startCheckServerSettings()
+            }
+        }
+    }
+
+    private fun needsAuthorization(): Boolean {
+        return (
+            account.incomingServerSettings.authenticationType == AuthType.XOAUTH2 ||
+                account.outgoingServerSettings.authenticationType == AuthType.XOAUTH2
+            ) &&
+            !authViewModel.isAuthorized(account)
+    }
+
+    private fun startCheckServerSettings() {
+        CheckAccountTask(account).executeOnExecutor(Executors.newSingleThreadExecutor(), direction)
     }
 
     private fun handleCertificateValidationException(exception: CertificateValidationException) {
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java
index 16decb409dc..b03cc1141e1 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java
@@ -151,9 +151,6 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
             }
         });
 
-        mAuthTypeAdapter = AuthTypeAdapter.get(this);
-        mAuthTypeView.setAdapter(mAuthTypeAdapter);
-
         /*
          * Only allow digits in the port field.
          */
@@ -172,6 +169,10 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
             mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
         }
 
+        boolean oAuthSupported = mAccount.getIncomingServerSettings().type.equals(Protocols.IMAP);
+        mAuthTypeAdapter = AuthTypeAdapter.get(this, oAuthSupported);
+        mAuthTypeView.setAdapter(mAuthTypeAdapter);
+
         boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
         if (editSettings) {
             TextInputLayoutHelper.configureAuthenticatedPasswordToggle(
@@ -407,17 +408,14 @@ protected void onPostCreate(Bundle savedInstanceState) {
      * Shows/hides password field and client certificate spinner
      */
     private void updateViewFromAuthType() {
-        AuthType authType = getSelectedAuthType();
-        boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
-
-        if (isAuthTypeExternal) {
-
-            // hide password fields, show client certificate fields
-            mPasswordLayoutView.setVisibility(View.GONE);
-        } else {
-
-            // show password fields, hide client certificate fields
-            mPasswordLayoutView.setVisibility(View.VISIBLE);
+        switch (getSelectedAuthType()) {
+            case EXTERNAL:
+            case XOAUTH2:
+                mPasswordLayoutView.setVisibility(View.GONE);
+                break;
+            default:
+                mPasswordLayoutView.setVisibility(View.VISIBLE);
+                break;
         }
     }
 
@@ -428,8 +426,9 @@ private void updateViewFromAuthType() {
     private void updateViewFromSecurity() {
         ConnectionSecurity security = getSelectedSecurity();
         boolean isUsingTLS = ((ConnectionSecurity.SSL_TLS_REQUIRED  == security) || (ConnectionSecurity.STARTTLS_REQUIRED == security));
+        boolean isUsingOAuth = getSelectedAuthType() == AuthType.XOAUTH2;
 
-        if (isUsingTLS) {
+        if (isUsingTLS && !isUsingOAuth) {
             mAllowClientCertificateView.setVisibility(View.VISIBLE);
         } else {
             mAllowClientCertificateView.setVisibility(View.GONE);
@@ -500,9 +499,13 @@ private void validateFields() {
                 && hasConnectionSecurity
                 && hasValidCertificateAlias;
 
+        boolean hasValidOAuthSettings = hasValidUserName
+                && hasConnectionSecurity
+                && authType == AuthType.XOAUTH2;
+
         mNextButton.setEnabled(Utility.domainFieldValid(mServerView)
                 && Utility.requiredFieldValid(mPortView)
-                && (hasValidPasswordSettings || hasValidExternalAuthSettings));
+                && (hasValidPasswordSettings || hasValidExternalAuthSettings || hasValidOAuthSettings));
         Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
     }
 
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java
index 8e64281ae7c..7eba7c81e8b 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java
@@ -127,7 +127,7 @@ public void onCreate(Bundle savedInstanceState) {
 
         mSecurityTypeView.setAdapter(ConnectionSecurityAdapter.get(this));
 
-        mAuthTypeAdapter = AuthTypeAdapter.get(this);
+        mAuthTypeAdapter = AuthTypeAdapter.get(this, true);
         mAuthTypeView.setAdapter(mAuthTypeAdapter);
 
         /*
@@ -162,8 +162,6 @@ public void onCreate(Bundle savedInstanceState) {
         try {
             ServerSettings settings = mAccount.getOutgoingServerSettings();
 
-            updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
-            updateViewFromSecurity(settings.connectionSecurity);
             if (savedInstanceState == null) {
                 // The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
                 mCurrentAuthTypeViewPosition = mAuthTypeAdapter.getAuthPosition(settings.authenticationType);
@@ -190,6 +188,9 @@ public void onCreate(Bundle savedInstanceState) {
             }
             mSecurityTypeView.setSelection(mCurrentSecurityTypeViewPosition, false);
 
+            updateAuthPlainTextFromSecurityType(getSelectedSecurity());
+            updateViewFromSecurity();
+
             if (!settings.username.isEmpty()) {
                 mUsernameView.setText(settings.username);
                 mRequireLoginView.setChecked(true);
@@ -251,7 +252,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position,
                  */
                 if (mCurrentSecurityTypeViewPosition != position) {
                     updatePortFromSecurityType();
-                    updateViewFromSecurity(getSelectedSecurity());
+                    updateViewFromSecurity();
                     boolean isInsecure = (ConnectionSecurity.NONE == getSelectedSecurity());
                     boolean isAuthExternal = (AuthType.EXTERNAL == getSelectedAuthType());
                     boolean loginNotRequired = !mRequireLoginView.isChecked();
@@ -292,6 +293,7 @@ public void onItemSelected(AdapterView<?> parent, View view, int position,
                 }
 
                 updateViewFromAuthType();
+                updateViewFromSecurity();
                 validateFields();
                 AuthType selection = getSelectedAuthType();
 
@@ -352,25 +354,26 @@ protected void onPostCreate(Bundle savedInstanceState) {
      * Shows/hides password field
      */
     private void updateViewFromAuthType() {
-        AuthType authType = getSelectedAuthType();
-        boolean isAuthTypeExternal = (AuthType.EXTERNAL == authType);
-
-        if (isAuthTypeExternal) {
-            // hide password fields
-            mPasswordLayoutView.setVisibility(View.GONE);
-        } else {
-            // show password fields
-            mPasswordLayoutView.setVisibility(View.VISIBLE);
+        switch (getSelectedAuthType()) {
+            case EXTERNAL:
+            case XOAUTH2:
+                mPasswordLayoutView.setVisibility(View.GONE);
+                break;
+            default:
+                mPasswordLayoutView.setVisibility(View.VISIBLE);
+                break;
         }
     }
 
     /**
      * Shows/hides client certificate spinner
      */
-    private void updateViewFromSecurity(ConnectionSecurity security) {
+    private void updateViewFromSecurity() {
+        ConnectionSecurity security = getSelectedSecurity();
         boolean isUsingTLS = ((ConnectionSecurity.SSL_TLS_REQUIRED  == security) || (ConnectionSecurity.STARTTLS_REQUIRED == security));
+        boolean isUsingOAuth = getSelectedAuthType() == AuthType.XOAUTH2;
 
-        if (isUsingTLS) {
+        if (isUsingTLS && !isUsingOAuth) {
             mAllowClientCertificateView.setVisibility(View.VISIBLE);
         } else {
             mAllowClientCertificateView.setVisibility(View.GONE);
@@ -406,7 +409,7 @@ private void validateFields() {
             mAuthTypeView.setSelection(mCurrentAuthTypeViewPosition, false);
             mAuthTypeView.setOnItemSelectedListener(onItemSelectedListener);
             updateViewFromAuthType();
-            updateViewFromSecurity(getSelectedSecurity());
+            updateViewFromSecurity();
 
             onItemSelectedListener = mSecurityTypeView.getOnItemSelectedListener();
             mSecurityTypeView.setOnItemSelectedListener(null);
@@ -441,11 +444,15 @@ private void validateFields() {
                 && hasConnectionSecurity
                 && hasValidCertificateAlias;
 
+        boolean hasValidOAuthSettings = hasValidUserName
+                && hasConnectionSecurity
+                && authType == AuthType.XOAUTH2;
+
         mNextButton
                 .setEnabled(Utility.domainFieldValid(mServerView)
                         && Utility.requiredFieldValid(mPortView)
                         && (!mRequireLoginView.isChecked()
-                                || hasValidPasswordSettings || hasValidExternalAuthSettings));
+                                || hasValidPasswordSettings || hasValidExternalAuthSettings || hasValidOAuthSettings));
         Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
     }
 
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java
index 53a6d557ea3..2df6c5e683d 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeAdapter.java
@@ -11,8 +11,14 @@ public AuthTypeAdapter(Context context, int resource, AuthTypeHolder[] holders)
         super(context, resource, holders);
     }
 
-    public static AuthTypeAdapter get(Context context) {
-        AuthType[] authTypes = new AuthType[]{AuthType.PLAIN, AuthType.CRAM_MD5, AuthType.EXTERNAL};
+    public static AuthTypeAdapter get(Context context, boolean oAuthSupported) {
+        AuthType[] authTypes;
+        if (oAuthSupported) {
+            authTypes = new AuthType[] { AuthType.PLAIN, AuthType.CRAM_MD5, AuthType.EXTERNAL, AuthType.XOAUTH2 };
+        } else {
+            authTypes = new AuthType[] { AuthType.PLAIN, AuthType.CRAM_MD5, AuthType.EXTERNAL };
+        }
+
         AuthTypeHolder[] holders = new AuthTypeHolder[authTypes.length];
         for (int i = 0; i < authTypes.length; i++) {
             holders[i] = new AuthTypeHolder(authTypes[i], context.getResources());
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java
index 998068d13ef..a12739d55a3 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthTypeHolder.java
@@ -41,7 +41,8 @@ private int resourceId() {
                 return R.string.account_setup_auth_type_encrypted_password;
             case EXTERNAL:
                 return R.string.account_setup_auth_type_tls_client_certificate;
-
+            case XOAUTH2:
+                return R.string.account_setup_auth_type_oauth2;
             case AUTOMATIC:
             case LOGIN:
             default:
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt
new file mode 100644
index 00000000000..44e101e15ba
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AuthViewModel.kt
@@ -0,0 +1,269 @@
+package com.fsck.k9.activity.setup
+
+import android.app.Activity
+import android.app.Application
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.ActivityResultRegistry
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.net.toUri
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.viewModelScope
+import com.fsck.k9.Account
+import com.fsck.k9.preferences.AccountManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.openid.appauth.AuthState
+import net.openid.appauth.AuthorizationException
+import net.openid.appauth.AuthorizationRequest
+import net.openid.appauth.AuthorizationResponse
+import net.openid.appauth.AuthorizationService
+import net.openid.appauth.AuthorizationServiceConfiguration
+import net.openid.appauth.ResponseTypeValues
+import timber.log.Timber
+
+private const val KEY_AUTHORIZATION = "app.k9mail_auth"
+
+class AuthViewModel(
+    application: Application,
+    private val accountManager: AccountManager,
+    private val oauthCredentials: OAuthCredentials
+) : AndroidViewModel(application) {
+    private var authService: AuthorizationService? = null
+    private val authState = AuthState()
+
+    private var account: Account? = null
+
+    private lateinit var resultObserver: AppAuthResultObserver
+
+    private val _uiState = MutableStateFlow<AuthFlowState>(AuthFlowState.Idle)
+    val uiState: StateFlow<AuthFlowState> = _uiState.asStateFlow()
+
+    @Synchronized
+    private fun getAuthService(): AuthorizationService {
+        return authService ?: AuthorizationService(getApplication<Application>()).also { authService = it }
+    }
+
+    fun init(activityResultRegistry: ActivityResultRegistry, lifecycle: Lifecycle) {
+        resultObserver = AppAuthResultObserver(activityResultRegistry)
+        lifecycle.addObserver(resultObserver)
+    }
+
+    fun authResultConsumed() {
+        _uiState.update { AuthFlowState.Idle }
+    }
+
+    fun isAuthorized(account: Account): Boolean {
+        val authState = getOrCreateAuthState(account)
+        return authState.isAuthorized
+    }
+
+    private fun getOrCreateAuthState(account: Account): AuthState {
+        return try {
+            account.oAuthState?.let { AuthState.jsonDeserialize(it) } ?: AuthState()
+        } catch (e: Exception) {
+            Timber.e(e, "Error deserializing AuthState")
+            AuthState()
+        }
+    }
+
+    fun login(account: Account) {
+        this.account = account
+
+        viewModelScope.launch {
+            val config = findOAuthConfiguration(account)
+            if (config == null) {
+                _uiState.update { AuthFlowState.NotSupported }
+                return@launch
+            }
+
+            try {
+                startLogin(account, config)
+            } catch (e: ActivityNotFoundException) {
+                _uiState.update { AuthFlowState.BrowserNotFound }
+            }
+        }
+    }
+
+    private suspend fun startLogin(account: Account, config: OAuthConfiguration) {
+        val authRequestIntent = withContext(Dispatchers.IO) {
+            createAuthorizationRequestIntent(account.email, config)
+        }
+
+        resultObserver.login(authRequestIntent)
+    }
+
+    private fun createAuthorizationRequestIntent(email: String, config: OAuthConfiguration): Intent {
+        val serviceConfig = AuthorizationServiceConfiguration(
+            config.authorizationEndpoint.toUri(),
+            config.tokenEndpoint.toUri()
+        )
+
+        val applicationId = getApplication<Application>().packageName
+        val redirectUri = Uri.parse("$applicationId:/oauth2redirect")
+
+        val authRequestBuilder = AuthorizationRequest.Builder(
+            serviceConfig,
+            config.clientId,
+            ResponseTypeValues.CODE,
+            redirectUri
+        )
+
+        val scopeString = config.scopes.joinToString(separator = " ")
+        val authRequest = authRequestBuilder
+            .setScope(scopeString)
+            .setLoginHint(email)
+            .build()
+
+        val authService = getAuthService()
+
+        return authService.getAuthorizationRequestIntent(authRequest)
+    }
+
+    private fun findOAuthConfiguration(account: Account): OAuthConfiguration? {
+        return when (account.incomingServerSettings.host) {
+            "imap.gmail.com", "imap.googlemail.com" -> {
+                OAuthConfiguration(
+                    clientId = oauthCredentials.gmailClientId,
+                    scopes = listOf("https://mail.google.com/"),
+                    authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
+                    tokenEndpoint = "https://oauth2.googleapis.com/token"
+                )
+            }
+            else -> null
+        }
+    }
+
+    private fun onLoginResult(authorizationResult: AuthorizationResult?) {
+        if (authorizationResult == null) {
+            _uiState.update { AuthFlowState.Canceled }
+            return
+        }
+
+        authorizationResult.response?.let { response ->
+            authState.update(authorizationResult.response, authorizationResult.exception)
+            exchangeToken(response)
+        }
+
+        authorizationResult.exception?.let { authorizationException ->
+            _uiState.update {
+                AuthFlowState.Failed(
+                    errorCode = authorizationException.error,
+                    errorMessage = authorizationException.errorDescription
+                )
+            }
+        }
+    }
+
+    private fun exchangeToken(response: AuthorizationResponse) {
+        viewModelScope.launch(Dispatchers.IO) {
+            val authService = getAuthService()
+
+            val tokenRequest = response.createTokenExchangeRequest()
+            authService.performTokenRequest(tokenRequest) { tokenResponse, authorizationException ->
+                authState.update(tokenResponse, authorizationException)
+
+                val account = account!!
+                account.oAuthState = authState.jsonSerializeString()
+
+                viewModelScope.launch(Dispatchers.IO) {
+                    accountManager.saveAccount(account)
+                }
+
+                if (authorizationException != null) {
+                    _uiState.update {
+                        AuthFlowState.Failed(
+                            errorCode = authorizationException.error,
+                            errorMessage = authorizationException.errorDescription
+                        )
+                    }
+                } else {
+                    _uiState.update { AuthFlowState.Success }
+                }
+            }
+        }
+    }
+
+    @Synchronized
+    override fun onCleared() {
+        authService?.dispose()
+        authService = null
+    }
+
+    inner class AppAuthResultObserver(private val registry: ActivityResultRegistry) : DefaultLifecycleObserver {
+        private var authorizationLauncher: ActivityResultLauncher<Intent>? = null
+        private var authRequestIntent: Intent? = null
+
+        override fun onCreate(owner: LifecycleOwner) {
+            authorizationLauncher = registry.register(KEY_AUTHORIZATION, AuthorizationContract(), ::onLoginResult)
+            authRequestIntent?.let { intent ->
+                authRequestIntent = null
+                login(intent)
+            }
+        }
+
+        override fun onDestroy(owner: LifecycleOwner) {
+            authorizationLauncher = null
+        }
+
+        fun login(authRequestIntent: Intent) {
+            val launcher = authorizationLauncher
+            if (launcher != null) {
+                launcher.launch(authRequestIntent)
+            } else {
+                this.authRequestIntent = authRequestIntent
+            }
+        }
+    }
+}
+
+private class AuthorizationContract : ActivityResultContract<Intent, AuthorizationResult?>() {
+    override fun createIntent(context: Context, input: Intent): Intent {
+        return input
+    }
+
+    override fun parseResult(resultCode: Int, intent: Intent?): AuthorizationResult? {
+        return if (resultCode == Activity.RESULT_OK && intent != null) {
+            AuthorizationResult(
+                response = AuthorizationResponse.fromIntent(intent),
+                exception = AuthorizationException.fromIntent(intent)
+            )
+        } else {
+            null
+        }
+    }
+}
+
+private data class AuthorizationResult(
+    val response: AuthorizationResponse?,
+    val exception: AuthorizationException?
+)
+
+sealed interface AuthFlowState {
+    object Idle : AuthFlowState
+
+    object Success : AuthFlowState
+
+    object NotSupported : AuthFlowState
+
+    object BrowserNotFound : AuthFlowState
+
+    object Canceled : AuthFlowState
+
+    data class Failed(val errorCode: String?, val errorMessage: String?) : AuthFlowState {
+        override fun toString(): String {
+            return listOfNotNull(errorCode, errorMessage).joinToString(separator = " - ")
+        }
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt
new file mode 100644
index 00000000000..0584046500e
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthConfiguration.kt
@@ -0,0 +1,8 @@
+package com.fsck.k9.activity.setup
+
+data class OAuthConfiguration(
+    val clientId: String,
+    val scopes: List<String>,
+    val authorizationEndpoint: String,
+    val tokenEndpoint: String
+)
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt
new file mode 100644
index 00000000000..23cdc6f894b
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/OAuthCredentials.kt
@@ -0,0 +1,5 @@
+package com.fsck.k9.activity.setup
+
+interface OAuthCredentials {
+    val gmailClientId: String
+}
diff --git a/app/ui/legacy/src/main/res/layout/account_setup_basics.xml b/app/ui/legacy/src/main/res/layout/account_setup_basics.xml
index 5c425f88f44..f88622f7f4c 100644
--- a/app/ui/legacy/src/main/res/layout/account_setup_basics.xml
+++ b/app/ui/legacy/src/main/res/layout/account_setup_basics.xml
@@ -37,6 +37,7 @@
             </com.google.android.material.textfield.TextInputLayout>
 
             <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/account_password_layout"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 app:passwordToggleEnabled="true">
diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml
index 54d46ca269b..0382d61edd8 100644
--- a/app/ui/legacy/src/main/res/values/strings.xml
+++ b/app/ui/legacy/src/main/res/values/strings.xml
@@ -355,6 +355,7 @@ Please submit bug reports, contribute new features and ask questions at
     <string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string>
     <string name="account_setup_auth_type_encrypted_password">Encrypted password</string>
     <string name="account_setup_auth_type_tls_client_certificate">Client certificate</string>
+    <string name="account_setup_auth_type_oauth2">OAuth 2.0</string>
 
     <string name="account_setup_incoming_title">Incoming server settings</string>
     <string name="account_setup_incoming_username_label">Username</string>
@@ -456,6 +457,10 @@ Please submit bug reports, contribute new features and ask questions at
     <string name="account_setup_failed_dlg_auth_message_fmt">Username or password incorrect.\n(<xliff:g id="error">%s</xliff:g>)</string>
     <string name="account_setup_failed_dlg_certificate_message_fmt">The server presented an invalid SSL certificate. Sometimes, this is because of a server misconfiguration. Sometimes it is because someone is trying to attack you or your mail server. If you\'re not sure what\'s up, click Reject and contact the folks who manage your mail server.\n\n(<xliff:g id="error">%s</xliff:g>)</string>
     <string name="account_setup_failed_dlg_server_message_fmt">Cannot connect to server.\n(<xliff:g id="error">%s</xliff:g>)</string>
+    <string name="account_setup_failed_dlg_oauth_flow_canceled">Authorization canceled</string>
+    <string name="account_setup_failed_dlg_oauth_flow_failed">Authorization failed with the following error: <xliff:g id="error">%s</xliff:g></string>
+    <string name="account_setup_failed_dlg_oauth_not_supported">OAuth 2.0 is currently not supported with this provider.</string>
+    <string name="account_setup_failed_dlg_browser_not_found">The app couldn\'t find a browser to use for granting access to your account.</string>
     <string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
     <string name="account_setup_failed_dlg_continue_action">Continue</string>
 
diff --git a/debug.keystore b/debug.keystore
new file mode 100644
index 00000000000..ddce9b12d43
Binary files /dev/null and b/debug.keystore differ
diff --git a/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java b/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java
index db7abfb0756..77ec7ecab10 100644
--- a/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java
+++ b/mail/common/src/main/java/com/fsck/k9/mail/oauth/OAuth2TokenProvider.java
@@ -1,8 +1,6 @@
 package com.fsck.k9.mail.oauth;
 
 
-import java.util.List;
-
 import com.fsck.k9.mail.AuthenticationFailedException;
 
 
@@ -12,16 +10,11 @@ public interface OAuth2TokenProvider {
      */
     int OAUTH2_TIMEOUT = 30000;
 
-    
-    /**
-     * @return Accounts suitable for OAuth 2.0 token provision.
-     */
-    List<String> getAccounts();
 
     /**
      * Fetch a token. No guarantees are provided for validity.
      */
-    String getToken(String username, long timeoutMillis) throws AuthenticationFailedException;
+    String getToken(long timeoutMillis) throws AuthenticationFailedException;
 
     /**
      * Invalidate the token for this username.
@@ -32,5 +25,5 @@ public interface OAuth2TokenProvider {
      * <p>
      * Invalidating a token and then failure with a new token should be treated as a permanent failure.
      */
-    void invalidateToken(String username);
+    void invalidateToken();
 }
diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java
index b949271e87a..057dbdb6a04 100644
--- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java
+++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapConnection.java
@@ -399,7 +399,7 @@ private List<ImapResponse> authXoauth2withSASLIR() throws IOException, Messaging
             return attemptXOAuth2();
         } catch (NegativeImapResponseException e) {
             //TODO: Check response code so we don't needlessly invalidate the token.
-            oauthTokenProvider.invalidateToken(settings.getUsername());
+            oauthTokenProvider.invalidateToken();
 
             if (!retryXoauth2WithNewToken) {
                 throw handlePermanentXoauth2Failure(e);
@@ -427,13 +427,13 @@ private List<ImapResponse> handleTemporaryXoauth2Failure(NegativeImapResponseExc
             //Okay, we failed on a new token.
             //Invalidate the token anyway but assume it's permanent.
             Timber.v(e, "Authentication exception for new token, permanent error assumed");
-            oauthTokenProvider.invalidateToken(settings.getUsername());
+            oauthTokenProvider.invalidateToken();
             throw handlePermanentXoauth2Failure(e2);
         }
     }
 
     private List<ImapResponse> attemptXOAuth2() throws MessagingException, IOException {
-        String token = oauthTokenProvider.getToken(settings.getUsername(), OAuth2TokenProvider.OAUTH2_TIMEOUT);
+        String token = oauthTokenProvider.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT);
         String authString = Authentication.computeXoauth(settings.getUsername(), token);
         String tag = sendSaslIrCommand(Commands.AUTHENTICATE_XOAUTH2, authString, true);
 
diff --git a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt
index 30fd795c5a7..9ed38fd8248 100644
--- a/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt
+++ b/mail/protocols/smtp/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.kt
@@ -559,7 +559,7 @@ class SmtpTransport(
                 throw negativeResponse
             }
 
-            oauthTokenProvider!!.invalidateToken(username)
+            oauthTokenProvider!!.invalidateToken()
 
             if (!retryXoauthWithNewToken) {
                 handlePermanentFailure(negativeResponse)
@@ -588,13 +588,13 @@ class SmtpTransport(
             // Okay, we failed on a new token. Invalidate the token anyway but assume it's permanent.
             Timber.v(negativeResponseFromNewToken, "Authentication exception for new token, permanent error assumed")
 
-            oauthTokenProvider!!.invalidateToken(username)
+            oauthTokenProvider!!.invalidateToken()
             handlePermanentFailure(negativeResponseFromNewToken)
         }
     }
 
     private fun attemptXoauth2(username: String) {
-        val token = oauthTokenProvider!!.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong())
+        val token = oauthTokenProvider!!.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong())
         val authString = Authentication.computeXoauth(username, token)
 
         val response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString)


Test patch:
diff --git a/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt b/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt
index b1bf51c5019..5f36f322391 100644
--- a/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt
+++ b/app/autodiscovery/providersxml/src/test/java/com/fsck/k9/autodiscovery/providersxml/ProvidersXmlDiscoveryTest.kt
@@ -20,13 +20,13 @@ class ProvidersXmlDiscoveryTest : RobolectricTest() {
         with(connectionSettings!!.incoming.first()) {
             assertThat(host).isEqualTo("imap.gmail.com")
             assertThat(security).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED)
-            assertThat(authType).isEqualTo(AuthType.PLAIN)
+            assertThat(authType).isEqualTo(AuthType.XOAUTH2)
             assertThat(username).isEqualTo("user@gmail.com")
         }
         with(connectionSettings.outgoing.first()) {
             assertThat(host).isEqualTo("smtp.gmail.com")
             assertThat(security).isEqualTo(ConnectionSecurity.SSL_TLS_REQUIRED)
-            assertThat(authType).isEqualTo(AuthType.PLAIN)
+            assertThat(authType).isEqualTo(AuthType.XOAUTH2)
             assertThat(username).isEqualTo("user@gmail.com")
         }
     }
diff --git a/app/ui/legacy/src/test/AndroidManifest.xml b/app/ui/legacy/src/test/AndroidManifest.xml
new file mode 100644
index 00000000000..9b1c30102c6
--- /dev/null
+++ b/app/ui/legacy/src/test/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application>
+        <!-- We're replacing this activity so we can run tests without the build tools complaining about a
+             missing manifest placeholder -->
+        <activity
+            android:name="net.openid.appauth.RedirectUriReceiverActivity"
+            android:exported="true"
+            tools:node="replace">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="dummy" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt
index eb86e916476..99bb4f45631 100644
--- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt
+++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapConnectionTest.kt
@@ -1045,8 +1045,7 @@ class RealImapConnectionTest {
 class TestTokenProvider : OAuth2TokenProvider {
     private var invalidationCount = 0
 
-    override fun getToken(username: String, timeoutMillis: Long): String {
-        assertThat(username).isEqualTo(USERNAME)
+    override fun getToken(timeoutMillis: Long): String {
         assertThat(timeoutMillis).isEqualTo(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong())
 
         return when (invalidationCount) {
@@ -1060,14 +1059,9 @@ class TestTokenProvider : OAuth2TokenProvider {
         }
     }
 
-    override fun invalidateToken(username: String) {
-        assertThat(username).isEqualTo(USERNAME)
+    override fun invalidateToken() {
         invalidationCount++
     }
-
-    override fun getAccounts(): List<String> {
-        throw UnsupportedOperationException()
-    }
 }
 
 private fun String.base64() = this.encodeUtf8().base64()
diff --git a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt
index be508368753..7819737d983 100644
--- a/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt
+++ b/mail/protocols/smtp/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.kt
@@ -18,10 +18,8 @@ import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.fail
 import org.junit.Test
 import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.ArgumentMatchers.anyString
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.doThrow
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.inOrder
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.stubbing
@@ -218,8 +216,8 @@ class SmtpTransportTest {
         }
 
         inOrder(oAuth2TokenProvider) {
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
-            verify(oAuth2TokenProvider).invalidateToken(USERNAME)
+            verify(oAuth2TokenProvider).getToken(anyLong())
+            verify(oAuth2TokenProvider).invalidateToken()
         }
         server.verifyConnectionClosed()
         server.verifyInteractionCompleted()
@@ -245,9 +243,9 @@ class SmtpTransportTest {
         transport.open()
 
         inOrder(oAuth2TokenProvider) {
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
-            verify(oAuth2TokenProvider).invalidateToken(USERNAME)
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
+            verify(oAuth2TokenProvider).getToken(anyLong())
+            verify(oAuth2TokenProvider).invalidateToken()
+            verify(oAuth2TokenProvider).getToken(anyLong())
         }
         server.verifyConnectionStillOpen()
         server.verifyInteractionCompleted()
@@ -273,9 +271,9 @@ class SmtpTransportTest {
         transport.open()
 
         inOrder(oAuth2TokenProvider) {
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
-            verify(oAuth2TokenProvider).invalidateToken(USERNAME)
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
+            verify(oAuth2TokenProvider).getToken(anyLong())
+            verify(oAuth2TokenProvider).invalidateToken()
+            verify(oAuth2TokenProvider).getToken(anyLong())
         }
         server.verifyConnectionStillOpen()
         server.verifyInteractionCompleted()
@@ -301,9 +299,9 @@ class SmtpTransportTest {
         transport.open()
 
         inOrder(oAuth2TokenProvider) {
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
-            verify(oAuth2TokenProvider).invalidateToken(USERNAME)
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
+            verify(oAuth2TokenProvider).getToken(anyLong())
+            verify(oAuth2TokenProvider).invalidateToken()
+            verify(oAuth2TokenProvider).getToken(anyLong())
         }
         server.verifyConnectionStillOpen()
         server.verifyInteractionCompleted()
@@ -357,7 +355,7 @@ class SmtpTransportTest {
             output("221 BYE")
         }
         stubbing(oAuth2TokenProvider) {
-            on { getToken(anyString(), anyLong()) } doThrow AuthenticationFailedException("Failed to fetch token")
+            on { getToken(anyLong()) } doThrow AuthenticationFailedException("Failed to fetch token")
         }
         val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2)
 
@@ -533,8 +531,8 @@ class SmtpTransportTest {
         }
 
         inOrder(oAuth2TokenProvider) {
-            verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyLong())
-            verify(oAuth2TokenProvider).invalidateToken(USERNAME)
+            verify(oAuth2TokenProvider).getToken(anyLong())
+            verify(oAuth2TokenProvider).invalidateToken()
         }
         server.verifyConnectionClosed()
         server.verifyInteractionCompleted()
@@ -950,7 +948,7 @@ class SmtpTransportTest {
 
     private fun createMockOAuth2TokenProvider(): OAuth2TokenProvider {
         return mock {
-            on { getToken(eq(USERNAME), anyLong()) } doReturn "oldToken" doReturn "newToken"
+            on { getToken(anyLong()) } doReturn "oldToken" doReturn "newToken"
         }
     }
 }