Sync from SUSE:SLFO:Main fetchmail revision 167eafefba1b0c2f31793088483240b6

This commit is contained in:
Adrian Schröter 2024-12-05 10:55:13 +01:00
commit 3e80a7d031
27 changed files with 4532 additions and 0 deletions

23
.gitattributes vendored Normal file
View File

@ -0,0 +1,23 @@
## Default LFS
*.7z filter=lfs diff=lfs merge=lfs -text
*.bsp filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.gem filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.jar filter=lfs diff=lfs merge=lfs -text
*.lz filter=lfs diff=lfs merge=lfs -text
*.lzma filter=lfs diff=lfs merge=lfs -text
*.obscpio filter=lfs diff=lfs merge=lfs -text
*.oxt filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.rpm filter=lfs diff=lfs merge=lfs -text
*.tbz filter=lfs diff=lfs merge=lfs -text
*.tbz2 filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.txz filter=lfs diff=lfs merge=lfs -text
*.whl filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text

View File

@ -0,0 +1,41 @@
# Patches fetchmail's behaviour for SMTP servers which:
# 1) insist on using TLS - we do not want messages which cannot
# be sent due to server negotiation problems lost - rather we
# keep them on the mailserver. As users should realize rather
# quickly that something went wrong, the overhead shouldn't
# be considered that important.
# 2) use the 501 error code to mark spam - in such a case
# the 'antispam' option should be used
Index: fetchmail-6.5.1/sink.c
===================================================================
--- fetchmail-6.5.1.orig/sink.c
+++ fetchmail-6.5.1/sink.c
@@ -526,6 +526,19 @@ static int handle_smtp_report(struct que
free(responses[0]);
return(PS_TRANSIENT);
+ case 501: /* Syntax error in parameters or arguments */
+ /*
+ * Some SMTP servers use this error code to refuse spam, however
+ * as we don't want to delete message if e.g. the address is (possibly
+ * by mistake) malformed, user has to use the 'antispam' option to
+ * treat this as spam symptom
+ */
+ free(responses[0]);
+ if (outlevel > O_SILENT)
+ report_complete(stdout,
+ GT_(" SMTP 501 error - if the server uses this code to report spam, include '501' in the 'antispam' option .\n"));
+ return(PS_TRANSIENT);
+
default:
/* bounce non-transient errors back to the sender */
if (smtperr >= 500 && smtperr <= 599)
@@ -591,7 +604,7 @@ static int handle_smtp_report_without_bo
case 553: /* invalid sending domain */
return(PS_REFUSED);
- default:
+ default:
/* bounce non-transient errors back to the sender */
if (smtperr >= 500 && smtperr <= 599)
return(PS_SUCCESS);

BIN
fetchmail-6.5.1.tar.xz (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEE3EplW9mTzUhx+oIQ5BKxVu/zhVoFAmcz6JcACgkQ5BKxVu/z
hVptQA//c7mklRvCJmSIhF91w0XntT5aOoWL19zLLZXBXgU+3YXmhu8sUufaZtVF
ORJpJPvxEuK3g3Ja9rS4q959pM1rTDZ15GvpnF9oXUi62xLEEGzMweU8PIIMu7Ov
AV6kJMJ0jmVEBwRhVnYn18+QgiB33hy6CiHbzKXQafElbSbR+7Cg7C4tPpv2hlfh
BslV/zZIGwDfi6GrlZqfPyd/r8258fhsmUQa/y/HTYm9QCc1DKXfSPzqpb1yZMLe
omOw0IMduE2xn2QsWz2CgdeNO7QPH6qFoM9SJTh3xtI4f7xVb6tLW4vvVpOQb85E
LIUg+NWoqBmLBx2wkLttSld9kDJZFTVksZEa7XcvJGY62tIeLAn/0Y1Ytg6Xpm9H
oihaNZ7vhsB6iwNWOuHxXtJOpvkMfEDmtSQDoT3ier+HA8/+ofiLUrctZ30uh0da
R/AjFDwFD53lbdWmfMpL+ZUHcctSg+3MDaL2AF2eOkxlDf0+zESffncxLLUXnhgR
5eSpxg/CclShPY1kkq6D3UTznwLu4UxsjL2YmQS0jefE4VrJOBFxpExMs2z/tMKn
yRV8JnDWTAHtw1FmynXz0YClXVHSKlf2h4vTUf92hapwvcz2ExhjkLDFiMMi+/jX
+Qlb4iYVUdg5rqvTCTgWoHC4nArbcSSlYrxSQZpjSzUzyJzsSX8=
=fYQt
-----END PGP SIGNATURE-----

View File

@ -0,0 +1,48 @@
From: Matthew Ogilvie <mmogilvi+fml@zoho.com>
Date: Sat, 3 Jun 2017 17:57:22 -0600
Subject: FAQ: list gmail options including oauthbearer and app password
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: dbeee6a0c0fc5392953f38d6f0dcffdeeb8ae141
---
fetchmail-FAQ.html | 24 +++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
Index: fetchmail-6.5.1/fetchmail-FAQ.html
===================================================================
--- fetchmail-6.5.1.orig/fetchmail-FAQ.html
+++ fetchmail-6.5.1/fetchmail-FAQ.html
@@ -1956,12 +1956,28 @@ authentication schemes based on OAuth 2.
users to jump through quite a few hoops, and use web browsers for
signing in, and software vendors to hand in their software for
sometimes paid reviews. Such is not going to happen for fetchmail.
+
+If this hinders access to your account through fetchmail, you have some
+options:</p>
+<ul>
+ <li>You can generate and use an
+ <a href="https://support.google.com/accounts/answer/185833">App Password</a>.
+ This is probably best unless you are on a "G-Suite" account and the
+ administrator has disabled this option.</li>
+ <li>You can use separate tools to generate and renew oauth2 access
+ tokens. Then configure fetchmail to use "auth oauthbearer" and use
+ a current access token as the password. See comments and --help in
+ contrib/fetchmail-oauth2.py from the fetchmail source tree
+ for more information. This is derived from Google's
+ <a href="https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough">OAuth2DotPyRunThrough</a>,
+ associated code, RFC-7628, and RFC-6750.</li>
+ <li>You may turn on access for "less secure apps" at
+ <a href="https://www.google.com/settings/security/lesssecureapps">https://www.google.com/settings/security/lesssecureapps</a>,
+ or see <a href="https://support.google.com/accounts/answer/6010255">https://support.google.com/accounts/answer/6010255</a>.
+ But G-suite administrators are more likely to have disabled
+ this option than "App Password"s.</li>
+</ul>
-If this hinders access to your account through fetchmail, you may
-need to turn on access for "less secure apps", or create an application or service specific password.
-
-For Google, this - at some point in time - used to live at <a
- href="https://myaccount.google.com/lesssecureapps">https://myaccount.google.com/lesssecureapps</a>.<br/>
It is disputable whether an application that does not include web
browsing capabilities or heavy-weight libraries is "less secure" as
Google claims.</p>

View File

@ -0,0 +1,616 @@
From: Matthew Ogilvie <mmogilvi+fml@zoho.com>
Date: Thu, 1 Jun 2017 00:09:02 -0600
Subject: add contrib/fetchnmail-oauth2.py token acquisition utility
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: c82625858682eb2396b6a49da79e403c6f2b018b
---
contrib/README | 6
contrib/fetchmail-oauth2.py | 567 ++++++++++++++++++++++++++++++++++++++++++++
fetchmail.man | 3
3 files changed, 575 insertions(+), 1 deletion(-)
create mode 100755 contrib/fetchmail-oauth2.py
Index: fetchmail-6.5.1/contrib/README
===================================================================
--- fetchmail-6.5.1.orig/contrib/README
+++ fetchmail-6.5.1/contrib/README
@@ -181,6 +181,12 @@ sendmail 8.11.0 with multidrop.
Watchdog script to check whether fetchmail is working in daemon mode.
+### fetchmail-oauth2.py
+
+Script to obtain oauth2 access tokens that "fetchmail --auth oauthbearer"
+expects in place of the password. See --help and comments in the
+script, as well as fetchmail --auth documentation.
+
### mold-remover.py
A short python script to remove old read mail from a pop3 mailserver.
Index: fetchmail-6.5.1/contrib/fetchmail-oauth2.py
===================================================================
--- /dev/null
+++ fetchmail-6.5.1/contrib/fetchmail-oauth2.py
@@ -0,0 +1,567 @@
+#!/usr/bin/python
+#
+# Updates: Copyright 2017 Matthew Ogilvie (mogilvie+fml at zoho.com)
+# - Started with https://github.com/google/gmail-oauth2-tools.git
+# commit 45c39795044c604ed126205806191a8473c0f671 dated
+# 2015-06-09.
+# - Add file interaction (--refresh, --auto_refresh,
+# --obtain_refresh_token_file and related options).
+# - Support both python 2 and 3.
+# - Keeping the same license (below).
+#
+# Copyright 2012 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+ # http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+###############
+# POSSIBLE IMPROVEMENTS:
+#
+# FUTURE: Explicitly track expiration time of access tokens,
+# and base --auto_refresh on actual expiration time instead of
+# simple age.
+# FUTURE: Add a mode that can print the access token by itself to
+# stdout, presumably piped into fetchmail or similar (either both
+# launched by a wrapper script, this launches fetchmail, or fetchmail
+# launches this).
+# FUTURE: Mix old and new interfaces (or get rid of old interface):
+# Support using a config file to supply some of the details
+# for the original google modes of operation (--generate_oauth2_token,
+# --generate_oauth2_string, --refresh_token, and --test_*).
+# Also support providing sensative data on the command line instead
+# of files for the new modes of operation, despite the lack
+# of security (process listings, .bash_history files, etc).
+# FUTURE: Revise model for how to set permissions on updated files?
+# Preserve existing? Somehow allow setting UID/GID? Warn if files
+# are accessible by anyone but the current user?
+
+"""Performs client tasks for testing IMAP OAuth2 authentication.
+
+This documentation and examples is for gmail. For other providers,
+you will likely need to track down appropriate non-default settings
+for auth_url, token_url, and scope.
+
+To use this script, you'll need to have registered with Google as an OAuth
+application and obtained an OAuth client ID and client secret.
+See https://developers.google.com/identity/protocols/OAuth2 and
+https://developers.google.com/identity/sign-in/web/devconsole-project
+for instructions on registering and for documentation of the APIs
+invoked by this code.
+
+This script has 2 main modes of operation.
+
+1. The first mode is used to generate and authorize an OAuth2 token, the
+first step in logging in via OAuth2.
+
+First, after registering your "application" (above) you should setup a
+configuration file. Use a text editor to do the command-line equivalent of:
+
+ sed 's/^ *//' > /path/to/oauth2Config.properties << EOF
+ client_id=1038[...].apps.googleusercontent.com
+ client_secret=VWFn8LIKAMC-MsjBMhJeOplZ
+ refresh_token_file=/home/path/to/refresh_token_file
+ access_token_file=/home/path/to/access_token_file
+EOF
+
+ chmod 600 /path/to/oauth2Config.properties
+
+Then run the following, and repeat any time the refresh token stops
+working, such as when you change your password. This is interactive
+and requires a web browser to complete:
+
+ oauth2 -c /path/to/oauth2Config.properties --obtain_refresh_token_file
+
+The script will converse with Google and generate an oauth request
+token, then present you with a URL you should visit in your browser to
+authorize the token. Once you get the verification code from the Google
+website, enter it into the script, which will then save access and referesh
+tokens to the corresponding files for later use.
+
+Also, you'll usually need to configure fetchmail by
+including a section like the following in your .fetchmailrc:
+
+ poll imap.gmail.com protocol imap
+ auth oauthbearer username "USER@gmail.com"
+ passwordfile "/home/path/to/access_token_file"
+ is LOCALUSER here sslmode wrapped sslcertck
+
+Alternative for debugging: You can also use the original google
+script interface to obtain these tokens without involving files:
+
+ oauth2 \
+ --client_id=1038[...].apps.googleusercontent.com \
+ --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
+ --generate_oauth2_token
+
+
+-----
+2. The script will generate new access tokens using a refresh token.
+
+This uses the same config file setup above.
+
+ oauth2 -c /path/to/oauth2Config.properties --auto_refresh
+ # Or force refresh by using --refresh instead of --auto_refresh.
+
+ fetchmail -s # or other tools configured to use the access_token_file
+ # And/or call something to update outgoing MTA relay configuration,
+ # if necessary.
+
+You may put this sequence in a short shell script,
+and configure cron to call it a few times per hour.
+
+Alternative for debugging: You can also use the original google
+script interface to refresh the token without involving files:
+
+ oauth2 \
+ --client_id=1038[...].apps.googleusercontent.com \
+ --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
+ --refresh_token=1/Yzm6MRy4q1xi7Dx2DuWXNgT6s37OrP_DW_IoyTum4YA
+
+-----
+Google's non-file script interface also supports a few other
+testing modes; see --help.
+"""
+
+from __future__ import print_function
+import base64
+import imaplib
+import json
+from optparse import OptionParser
+import smtplib
+import sys
+import os
+import time
+
+try:
+ import urllib.request as urlopen
+ import urllib.parse as urlparse
+except ImportError:
+ import urllib as urlopen
+ import urllib as urlparse
+
+try: input = raw_input
+except NameError: pass
+
+
+def SetupOptionParser():
+ # Usage message is the module's docstring.
+ parser = OptionParser(usage=__doc__)
+ parser.add_option('-c', '--config_file',
+ default=None,
+ help='Configuration file for --refresh '
+ 'and --obtain_refresh_token_file.\n'
+ 'The file should contain 4 (or more) settings, '
+ 'one per line, or they can also be overridden '
+ 'by the equivalent options:\n'
+ ' client_id=...\n'
+ ' client_secret=...\n'
+ ' refresh_token_file=/path/to/...\n'
+ ' access_token_file=/path/to/...\n'
+ ' Also max_age_sec, scope, umask, auth_url, and'
+ ' token_url have reasonable defaults for google.')
+ parser.add_option('--auto_refresh',
+ action='store_const',
+ default=None,
+ const=1,
+ dest='refresh',
+ help='Automatically refresh access_token_file, '
+ 'if older than max_age_sec from '
+ 'required -c /file/ info.');
+ parser.add_option('--refresh',
+ action='store_const',
+ const=2,
+ dest='refresh',
+ help='Refresh access_token_file '
+ 'unconditionally. Requires -c /file/ info.');
+ parser.add_option('--obtain_refresh_token_file',
+ action='store_true',
+ dest='obtain_refresh_token_file',
+ default=None,
+ help='Update refresh token in file. This is '
+ 'interactive, and requires '
+ 'a web browser. Also requires -c /file/ info. '
+ 'This also saves an initial access_token_file.');
+ parser.add_option('--client_id',
+ default=None,
+ help='Client ID of the application that is authenticating. '
+ 'See OAuth2 documentation for details.')
+ parser.add_option('--client_secret',
+ default=None,
+ help='Client secret of the application that is '
+ 'authenticating. See OAuth2 documentation for '
+ 'details.')
+ parser.add_option('--access_token_file',
+ default=None,
+ help='File name containing OAuth2 access token')
+ parser.add_option('--refresh_token_file',
+ default=None,
+ help='File name containing OAuth2 refresh token')
+ parser.add_option('--max_age_sec',
+ default=None, # manual default 3000
+ help='default max file age for --auto_refresh. '
+ 'Defaults to 3000 (10 minutes short of '
+ 'normal 3600 sec token expiration).')
+ parser.add_option('--umask',
+ default=None, # manual default 0077
+ help='default umask for --auto_refresh and '
+ '--obtain_refresh_token_file. Defaults to 0077.')
+ parser.add_option('--scope',
+ default=None, # manual default='https://mail.google.com/'
+ help='scope for the access token. Multiple scopes can be '
+ 'listed separated by spaces with the whole argument '
+ 'quoted. Defaults to https://mail.google.com/')
+ parser.add_option('--auth_url',
+ default=None, # manual default...
+ help='Permission URL for --obtain_refresh_token_file. '
+ 'Defaults to https://accounts.google.com/o/oauth2/auth.')
+ parser.add_option('--token_url',
+ default=None, # manual default...
+ help='Token URL for --obtain_refresh_token_file,'
+ ' and --refresh. '
+ 'Defaults to https://accounts.google.com/o/oauth2/token.')
+ parser.add_option('--generate_oauth2_token',
+ action='store_true',
+ dest='generate_oauth2_token',
+ help='(OLD/testing) generates an OAuth2 token for testing.'
+ ' Ignores all files.')
+ parser.add_option('--refresh_token',
+ default=None,
+ help='(OLD/testing) Generate a new access token using'
+ ' this OAuth2 refresh token. Ignores all files.')
+ parser.add_option('--user',
+ default=None,
+ help='(OLD/testing) email address of user whose account'
+ ' is being accessed')
+ parser.add_option('--access_token',
+ default=None,
+ help='(OLD/testing) OAuth2 access token.')
+ parser.add_option('--generate_oauth2_string',
+ action='store_true',
+ dest='generate_oauth2_string',
+ help='(OLD/testing) generates an initial client response'
+ ' string for OAuth2. Ignores all files.')
+ parser.add_option('--test_imap_authentication',
+ action='store_true',
+ dest='test_imap_authentication',
+ help='(OLD/testing) attempts to authenticate to IMAP. '
+ 'Ignores all files.')
+ parser.add_option('--test_smtp_authentication',
+ action='store_true',
+ dest='test_smtp_authentication',
+ help='(OLD/testing) attempts to authenticate to SMTP. '
+ 'Ignores all files.')
+ return parser
+
+
+# Hardcoded dummy redirect URI for non-web apps.
+REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
+
+
+def UrlEscape(text):
+ # See OAUTH 5.1 for a definition of which characters need to be escaped.
+ return urlparse.quote(text, safe='~-._')
+
+
+def UrlUnescape(text):
+ # See OAUTH 5.1 for a definition of which characters need to be escaped.
+ return urlparse.unquote(text)
+
+
+def FormatUrlParams(params):
+ """Formats parameters into a URL query string.
+
+ Args:
+ params: A key-value map.
+
+ Returns:
+ A URL query string version of the given parameters.
+ """
+ param_fragments = []
+ for param in sorted(params.items(), key=lambda x: x[0]):
+ param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1])))
+ return '&'.join(param_fragments)
+
+
+def GeneratePermissionUrl(client_id, scope, auth_url):
+ """Generates the URL for authorizing access.
+
+ This uses the "OAuth2 for Installed Applications" flow described at
+ https://developers.google.com/accounts/docs/OAuth2InstalledApp
+
+ Args:
+ client_id: Client ID obtained by registering your app.
+ scope: scope for access token, e.g. 'https://mail.google.com'
+ Returns:
+ A URL that the user should visit in their browser.
+ """
+ if not scope:
+ scope = 'https://mail.google.com/'
+ if not auth_url:
+ auth_url = 'https://accounts.google.com/o/oauth2/auth'
+ params = {}
+ params['client_id'] = client_id
+ params['redirect_uri'] = REDIRECT_URI
+ params['scope'] = scope
+ params['response_type'] = 'code'
+ return '%s?%s' % (auth_url, FormatUrlParams(params))
+
+
+def AuthorizeTokens(client_id, client_secret, authorization_code, token_url):
+ """Obtains OAuth access token and refresh token.
+
+ This uses the application portion of the "OAuth2 for Installed Applications"
+ flow at https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse
+
+ Args:
+ client_id: Client ID obtained by registering your app.
+ client_secret: Client secret obtained by registering your app.
+ authorization_code: code generated by Google Accounts after user grants
+ permission.
+ Returns:
+ The decoded response from the Google Accounts server, as a dict. Expected
+ fields include 'access_token', 'expires_in', and 'refresh_token'.
+ """
+ params = {}
+ params['client_id'] = client_id
+ params['client_secret'] = client_secret
+ params['code'] = authorization_code
+ params['redirect_uri'] = REDIRECT_URI
+ params['grant_type'] = 'authorization_code'
+ if not token_url:
+ token_url = 'https://accounts.google.com/o/oauth2/token'
+
+ response = urlopen.urlopen(token_url,
+ urlparse.urlencode(params).encode('ascii')).read()
+ return json.loads(response.decode("utf-8"))
+
+
+def RefreshToken(client_id, client_secret, refresh_token, token_url):
+ """Obtains a new token given a refresh token.
+
+ See https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh
+
+ Args:
+ client_id: Client ID obtained by registering your app.
+ client_secret: Client secret obtained by registering your app.
+ refresh_token: A previously-obtained refresh token.
+ Returns:
+ The decoded response from the Google Accounts server, as a dict. Expected
+ fields include 'access_token', 'expires_in', and 'refresh_token'.
+ """
+ params = {}
+ params['client_id'] = client_id
+ params['client_secret'] = client_secret
+ params['refresh_token'] = refresh_token
+ params['grant_type'] = 'refresh_token'
+ if not token_url:
+ token_url = 'https://accounts.google.com/o/oauth2/token'
+
+ response = urlopen.urlopen(token_url,
+ urlparse.urlencode(params).encode('ascii')).read()
+ return json.loads(response.decode("utf-8"))
+
+
+def GenerateOAuth2String(username, access_token, base64_encode=True):
+ """Generates an IMAP OAuth2 authentication string.
+
+ See https://developers.google.com/google-apps/gmail/oauth2_overview
+
+ Args:
+ username: the username (email address) of the account to authenticate
+ access_token: An OAuth2 access token.
+ base64_encode: Whether to base64-encode the output.
+
+ Returns:
+ The SASL argument for the OAuth2 mechanism.
+ """
+ auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
+ if base64_encode:
+ auth_string = base64.b64encode(auth_string)
+ return auth_string
+
+
+def TestImapAuthentication(user, auth_string):
+ """Authenticates to IMAP with the given auth_string.
+
+ Prints a debug trace of the attempted IMAP connection.
+
+ Args:
+ user: The Gmail username (full email address)
+ auth_string: A valid OAuth2 string, as returned by GenerateOAuth2String.
+ Must not be base64-encoded, since imaplib does its own base64-encoding.
+ """
+ print()
+ imap_conn = imaplib.IMAP4_SSL('imap.gmail.com')
+ imap_conn.debug = 4
+ imap_conn.authenticate('XOAUTH2', lambda x: auth_string)
+ imap_conn.select('INBOX')
+
+
+def TestSmtpAuthentication(user, auth_string):
+ """Authenticates to SMTP with the given auth_string.
+
+ Args:
+ user: The Gmail username (full email address)
+ auth_string: A valid OAuth2 string, not base64-encoded, as returned by
+ GenerateOAuth2String.
+ """
+ print()
+ smtp_conn = smtplib.SMTP('smtp.gmail.com', 587)
+ smtp_conn.set_debuglevel(True)
+ smtp_conn.ehlo('test')
+ smtp_conn.starttls()
+ smtp_conn.docmd('AUTH', 'XOAUTH2 ' + base64.b64encode(auth_string))
+
+
+def RequireOptions(options, *args):
+ missing = [arg for arg in args if getattr(options, arg) is None]
+ if missing:
+ print('Missing options: %s' % ' '.join(missing))
+ sys.exit(-1)
+
+def parseConfigFile(options):
+ if options.config_file:
+ cfg = dict(line.strip().split('=',1) for line in open(options.config_file))
+ else:
+ cfg = { }
+ # defaults:
+ if not 'scope' in cfg:
+ cfg['scope'] = 'https://mail.google.com/'
+ if not 'max_age_sec' in cfg:
+ cfg['max_age_sec'] = '3000'
+ if not 'umask' in cfg:
+ cfg['umask'] = '0077'
+ if not 'auth_url' in cfg:
+ cfg['auth_url'] = 'https://accounts.google.com/o/oauth2/auth'
+ if not 'token_url' in cfg:
+ cfg['token_url'] = 'https://accounts.google.com/o/oauth2/token'
+ # overrides (from command line):
+ for arg in [ 'scope', 'client_id', 'client_secret', 'umask',
+ 'max_age_sec', 'access_token_file', 'refresh_token_file',
+ 'auth_url', 'token_url' ]:
+ if getattr(options,arg):
+ cfg[arg] = getattr(options,arg)
+ return cfg
+
+def requireConfig(cfg, *args):
+ missing = [arg for arg in args if not arg in cfg]
+ if missing:
+ print('Missing options: %s' % ' '.join(missing))
+ sys.exit(-1)
+
+
+def main(argv):
+ options_parser = SetupOptionParser()
+ (options, args) = options_parser.parse_args()
+ if options.refresh:
+ cfg = parseConfigFile(options)
+ requireConfig(cfg, 'refresh_token_file', 'access_token_file',
+ 'client_id', 'client_secret', 'umask')
+ st = os.stat(cfg['access_token_file'])
+ if options.refresh < 2:
+ requireConfig(cfg, 'max_age_sec')
+ if time.time()-st.st_mtime < int(cfg['max_age_sec']):
+ return
+ with open(cfg['refresh_token_file'],"r") as f:
+ reftok = f.readline().rstrip()
+ if len(reftok) == 0:
+ print('refresh token is empty')
+ sys.exit(-1)
+ response = RefreshToken(cfg['client_id'],cfg['client_secret'],reftok,
+ cfg['token_url'])
+ newTok = response['access_token']
+ if len(newTok) == 0:
+ print('failed to obtain access token: it is empty')
+ sys.exit(-1)
+ savedUmask = os.umask(int(cfg['umask'],8))
+ try:
+ with open(cfg['access_token_file']+".tmp","w") as f:
+ f.write(newTok)
+ f.write('\n')
+ os.rename(cfg['access_token_file']+".tmp",cfg['access_token_file'])
+ finally:
+ os.umask(savedUmask)
+ elif options.obtain_refresh_token_file:
+ cfg = parseConfigFile(options)
+ requireConfig(cfg, 'refresh_token_file', 'access_token_file',
+ 'client_id', 'client_secret', 'umask')
+ print('To authorize token, visit this url and follow the directions:')
+ print(' %s' % GeneratePermissionUrl(cfg['client_id'], cfg['scope'],
+ cfg['auth_url']))
+ authorization_code = input('Enter verification code: ')
+ response = AuthorizeTokens(cfg['client_id'], cfg['client_secret'],
+ authorization_code, cfg['token_url'])
+ newRefTok = response['refresh_token']
+ if len(newRefTok) == 0:
+ print('failed to obtain refresh token: it is empty')
+ sys.exit(-1)
+ newTok = response['access_token']
+ if len(newTok) == 0:
+ print('failed to obtain corresponding access token: it is empty')
+ sys.exit(-1)
+ savedUmask = os.umask(int(cfg['umask'],8))
+ try:
+ with open(cfg['refresh_token_file']+".tmp","w") as f:
+ f.write(newRefTok)
+ f.write('\n')
+ os.rename(cfg['refresh_token_file']+".tmp",cfg['refresh_token_file'])
+ with open(cfg['access_token_file']+".tmp","w") as f:
+ f.write(newTok)
+ f.write('\n')
+ print("Refresh token saved to '%s'" % cfg['refresh_token_file'])
+ print("Initial access token saved to '%s'" % cfg['access_token_file'])
+ print('Access Token Expiration Seconds: %s' % response['expires_in'])
+ os.rename(cfg['access_token_file']+".tmp",cfg['access_token_file'])
+ finally:
+ os.umask(savedUmask)
+
+ ##### (OLD/testing options)
+
+ elif options.refresh_token:
+ RequireOptions(options, 'client_id', 'client_secret')
+ response = RefreshToken(options.client_id, options.client_secret,
+ options.refresh_token, options.token_url)
+ print('Access Token: %s' % response['access_token'])
+ print('Access Token Expiration Seconds: %s' % response['expires_in'])
+ elif options.generate_oauth2_string:
+ RequireOptions(options, 'user', 'access_token')
+ print ('OAuth2 argument:\n' +
+ GenerateOAuth2String(options.user, options.access_token))
+ elif options.generate_oauth2_token:
+ RequireOptions(options, 'client_id', 'client_secret')
+ print('To authorize token, visit this url and follow the directions:')
+ print(' %s' % GeneratePermissionUrl(options.client_id, options.scope,
+ options.auth_url))
+ authorization_code = input('Enter verification code: ')
+ response = AuthorizeTokens(options.client_id, options.client_secret,
+ authorization_code, options.token_url)
+ print('Refresh Token: %s' % response['refresh_token'])
+ print('Access Token: %s' % response['access_token'])
+ print('Access Token Expiration Seconds: %s' % response['expires_in'])
+ elif options.test_imap_authentication:
+ RequireOptions(options, 'user', 'access_token')
+ TestImapAuthentication(options.user,
+ GenerateOAuth2String(options.user, options.access_token,
+ base64_encode=False))
+ elif options.test_smtp_authentication:
+ RequireOptions(options, 'user', 'access_token')
+ TestSmtpAuthentication(options.user,
+ GenerateOAuth2String(options.user, options.access_token,
+ base64_encode=False))
+ else:
+ options_parser.print_help()
+ print('Nothing to do, exiting.')
+ return
+
+
+if __name__ == '__main__':
+ main(sys.argv)
Index: fetchmail-6.5.1/fetchmail.man
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.man
+++ fetchmail-6.5.1/fetchmail.man
@@ -1181,7 +1181,8 @@ External tools are necessary to obtain
such tokens. Access tokens often expire fairly quickly (e.g. 1 hour),
and new ones need to be generated from renewal tokens, so the
"passwordfile", "passwordfd", or "pwmd_*" options may be useful. See the
-oauth2.py script from
+contrib/fetchmail-oauth2.py script from the fetchmail source code, which
+was derived from code associated with
.URL https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough "Google's Oauth2 Run Through" ,
and other oauth2 documentation. For services like gmail, an "App Password"
is probably preferable if available, because it has roughly the same

View File

@ -0,0 +1,300 @@
From: Matthew Ogilvie <mmogilvi+fml@zoho.com>
Date: Sat, 27 May 2017 15:32:28 -0600
Subject: add imap oauthbearer support
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: 5c44df6df70b90f06d3204c6fbdd1ff19e990ca0
This expects an oauth2 access token in place of password.
When configured, it will also fall back on trying xoauth2.
---
conf.c | 2 +
fetchmail.c | 3 +
fetchmail.h | 2 +
fetchmail.man | 26 +++++++++++--
fetchmailconf.py | 2 -
imap.c | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
options.c | 2 +
rcfile_l.l | 1
8 files changed, 137 insertions(+), 5 deletions(-)
Index: fetchmail-6.5.1/conf.c
===================================================================
--- fetchmail-6.5.1.orig/conf.c
+++ fetchmail-6.5.1/conf.c
@@ -284,6 +284,8 @@ void dump_config(struct runctl *runp, st
stringdump("auth", "otp");
else if (ctl->server.authenticate == A_MSN)
stringdump("auth", "msn");
+ else if (ctl->server.authenticate == A_OAUTHBEARER)
+ stringdump("auth", "oauthbearer");
#ifdef HAVE_RES_SEARCH
booldump("dns", ctl->server.dns);
Index: fetchmail-6.5.1/fetchmail.c
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.c
+++ fetchmail-6.5.1/fetchmail.c
@@ -1804,6 +1804,9 @@ static void dump_params (struct runctl *
case A_IMPLICIT:
printf(GT_(" End-to-end encryption assumed.\n"));
break;
+ case A_OAUTHBEARER:
+ printf(GT_(" OAUTHBEARER will be forced; expecting password to really be OAUTH2 authentication token\n"));
+ break;
}
if (ctl->server.principal != (char *) NULL)
printf(GT_(" Mail service principal is: %s\n"), ctl->server.principal);
Index: fetchmail-6.5.1/fetchmail.h
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.h
+++ fetchmail-6.5.1/fetchmail.h
@@ -64,6 +64,7 @@ struct addrinfo;
#define A_IMPLICIT 8 /* authentication at session level */
#define A_MSN 9 /* same as NTLM with keyword MSN */
#define A_EXTERNAL 10 /* external authentication (client cert) */
+#define A_OAUTHBEARER 11 /** oauth2 access token (not password) */
/* some protocols or authentication types (KERBEROS, GSSAPI, SSH) don't
* require a password */
@@ -99,6 +100,7 @@ struct addrinfo;
#define MSGBUFSIZE 8192
#define NAMELEN 64 /* max username length */
+/* oauth2 access tokens seem to be about 130 characters; make this longer: */
#define PASSWORDLEN 256 /* max password length */
#define DIGESTLEN 33 /* length of MD5 digest */
Index: fetchmail-6.5.1/fetchmail.man
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.man
+++ fetchmail-6.5.1/fetchmail.man
@@ -1113,8 +1113,8 @@ AUTHENTICATION below for details). The
\&\fBpassword\fP, \fBkerberos_v5\fP, \fBkerberos\fP (or, for
excruciating exactness, \fBkerberos_v4\fP), \fBgssapi\fP,
\fBcram\-md5\fP, \fBotp\fP, \fBntlm\fP, \fBmsn\fP (only for POP3),
-\fBexternal\fP (only IMAP) and \fBimplicit\fP (\fBssh\fP is understood
-as alias for \fBimplicit\fP).
+\fBexternal\fP (only IMAP), \fBimplicit\fP (\fBssh\fP is understood
+as alias for \fBimplicit\fP) and \fBoauthbearer\fP (only IMAP).
When \fBany\fP (the default) is specified, fetchmail tries
first methods that do not require a password (EXTERNAL, GSSAPI, KERBEROS\ IV,
KERBEROS\ 5); then it looks for methods that mask your password
@@ -1139,8 +1139,24 @@ authentication. This option does not wo
in line with RFC-2743 and IANA registrations, see
.UR https://www.iana.org/assignments/gssapi-service-names/
Generic Security Service Application Program Interface (GSSAPI)/Kerberos/Simple
-Authentication and Security Layer (SASL) Service Names
-.UE .
+Authentication and Security Layer (SASL) Service Names .
+.sp
+\fBoauthbearer\fP expects the supplied password to be an oauth2 authentication
+token instead of a password, as used by services like gmail.
+See RFC 7628 and RFC 6750. The \fBoauthbearer\fP
+setting also allows the non-standard "xoauth2" SASL scheme (using
+the same token) if the server only claims to support "xoauth2".
+External tools are necessary to obtain
+such tokens. Access tokens often expire fairly quickly (e.g. 1 hour),
+and new ones need to be generated from renewal tokens. See the
+oauth2.py script from
+.URL https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough "Google's Oauth2 Run Through" ,
+and other oauth2 documentation. For services like gmail, an "App Password"
+is probably preferable if available, because it has roughly the same
+security risks, and is a whole lot simpler to get working. "App Password"
+and oauthbearer both need to protect secrets on the client machine (files) and
+over the network (SSL/TLS). But "App Password" is
+sometimes completely disabled by business "G-suite" administrators.
.SS Miscellaneous Options
.TP
.B \-f <pathname> | \-\-fetchmailrc <pathname>
@@ -2475,7 +2491,9 @@ Legal protocol identifiers for use with
.PP
Legal authentication types are 'any', 'password', 'kerberos',
\&'kerberos_v4', 'kerberos_v5' and 'gssapi', 'cram\-md5', 'otp', 'msn'
-(only for POP3), 'ntlm', 'implicit', 'external' (only IMAP).
+(only for POP3), 'ntlm', 'implicit', 'external' (only IMAP),
+'oauthbearer' (only for IMAP; requires authentication token in
+place of password).
The 'password' type specifies
authentication by normal transmission of a password (the password may be
plain text or subject to protocol-specific encryption as in CRAM-MD5);
Index: fetchmail-6.5.1/fetchmailconf.py
===================================================================
--- fetchmail-6.5.1.orig/fetchmailconf.py
+++ fetchmail-6.5.1/fetchmailconf.py
@@ -499,7 +499,7 @@ defaultports = {"auto":None,
"ODMR":"odmr"}
authlist = ("any", "password", "gssapi", "kerberos", "implicit", "otp",
- "msn", "ntlm")
+ "msn", "ntlm", "oauthbearer")
listboxhelp = {
'title' : 'List Selection Help',
Index: fetchmail-6.5.1/imap.c
===================================================================
--- fetchmail-6.5.1.orig/imap.c
+++ fetchmail-6.5.1/imap.c
@@ -24,6 +24,10 @@
#define IMAP4 0 /* IMAP4 rev 0, RFC1730 */
#define IMAP4rev1 1 /* IMAP4 rev 1, RFC2060 */
+/* imap_plus_cont_context values */
+#define IPLUS_NONE 0
+#define IPLUS_OAUTHBEARER 1 /* oauthbearer (for more error info) */
+
/* global variables: please reinitialize them explicitly for proper
* working in daemon mode */
@@ -49,6 +53,8 @@ static void clear_sessiondata(void) {
* a const initializer */
const char *const capa_begin = " [CAPABILITY "; const unsigned capa_len = 13;
+static int plus_cont_context = IPLUS_NONE;
+
/* mailbox variables initialized in imap_getrange() */
static int count = 0, oldcount = 0, recentcount = 0, unseen = 0, deletions = 0;
static unsigned int startcount = 1;
@@ -262,6 +268,21 @@ static int imap_response(int sock, char
if (ok != PS_SUCCESS)
return(ok);
+ if (buf[0] == '+' && buf[1] == ' ') {
+ if (plus_cont_context == IPLUS_OAUTHBEARER) {
+ /* future: Consider decoding the base64-encoded JSON
+ * error response info and logging it. But for now,
+ * ignore continuation data, send the expected blank
+ * line, and assume that the next response will be
+ * a tagged "NO" as documented.
+ */
+ SockWrite(sock, "\r\n", 2);
+ if (outlevel >= O_MONITOR)
+ report(stdout, "IMAP> \n");
+ continue;
+ }
+ }
+
/* all tokens in responses are caseblind */
for (cp = buf; *cp; cp++)
if (islower((unsigned char)*cp))
@@ -394,6 +415,69 @@ static int do_imap_ntlm(int sock, struct
}
#endif /* NTLM */
+static int do_imap_oauthbearer(int sock, struct query *ctl,flag xoauth2)
+{
+ /* Implements relevant parts of RFC-7628, RFC-6750, and
+ * https://developers.google.com/gmail/imap/xoauth2-protocol
+ *
+ * This assumes something external manages obtaining an up-to-date
+ * authentication/bearer token and arranging for it to be in
+ * ctl->password. This may involve renewing it ahead of time if
+ * necessary using a renewal token that fetchmail knows nothing about.
+ * See:
+ * https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough
+ */
+ const char *name;
+ char *oauth2str;
+ int oauth2len;
+ int saved_suppress_tags = suppress_tags;
+
+ char *oauth2b64;
+
+ int ok;
+
+ oauth2len = strlen(ctl->remotename) + strlen(ctl->password) + 32;
+ oauth2str = (char *)xmalloc(oauth2len);
+ if (xoauth2)
+ {
+ snprintf(oauth2str, oauth2len,
+ "user=%s\1auth=Bearer %s\1\1",
+ ctl->remotename,
+ ctl->password);
+ name = "XOAUTH2";
+ }
+ else
+ {
+ snprintf(oauth2str, oauth2len,
+ "n,a=%s,\1auth=Bearer %s\1\1",
+ ctl->remotename,
+ ctl->password);
+ name = "OAUTHBEARER";
+ }
+
+ oauth2b64 = (char *)xmalloc(2*strlen(oauth2str)+8);
+ to64frombits(oauth2b64, oauth2str, strlen(oauth2str));
+
+ memset(oauth2str, 0x55, strlen(oauth2str));
+ free(oauth2str);
+
+ /* Protect the access token like a password in logs, despite the
+ * usually-short expiration time and base64 encoding:
+ */
+ strlcpy(shroud, oauth2b64, sizeof(shroud));
+
+ plus_cont_context = IPLUS_OAUTHBEARER;
+ ok = gen_transact(sock, "AUTHENTICATE %s %s", name, oauth2b64);
+ plus_cont_context = IPLUS_NONE;
+
+ memset(shroud, 0x55, sizeof(shroud));
+ shroud[0] = '\0';
+ memset(oauth2b64, 0x55, strlen(oauth2b64));
+ free(oauth2b64);
+
+ return ok;
+}
+
static void imap_canonicalize(char *result, char *raw, size_t maxlen)
/* encode an IMAP password as per RFC1730's quoting conventions */
{
@@ -584,6 +668,26 @@ static int imap_getauth(int sock, struct
for future maintenance */
(void)ok;
+ if (ctl->server.authenticate == A_OAUTHBEARER)
+ {
+ /* Fetchmail's oauthbearer and xoauth2 support expects the "password"
+ * to actually be an oauth2 authentication token, so only
+ * try these options if specifically enabled.
+ * (Generating a token using the complex https-based oauth2
+ * protocol is left as an exercise for the user.)
+ */
+ if (strstr(capabilities, "AUTH=OAUTHBEARER") ||
+ !strstr(capabilities, "AUTH=XOAUTH2"))
+ {
+ ok = do_imap_oauthbearer(sock, ctl, FALSE); /* OAUTHBEARER */
+ }
+ if (ok && strstr(capabilities, "AUTH=XOAUTH2"))
+ {
+ ok = do_imap_oauthbearer(sock, ctl, TRUE); /* XOAUTH2 */
+ }
+ return ok;
+ }
+
/* Yahoo hack - we'll just try ID if it was offered by the server,
* and IGNORE errors. */
{
Index: fetchmail-6.5.1/options.c
===================================================================
--- fetchmail-6.5.1.orig/options.c
+++ fetchmail-6.5.1/options.c
@@ -395,6 +395,8 @@ int parsecmdline (int argc /** argument
ctl->server.authenticate = A_ANY;
else if (strcmp(optarg, "msn") == 0)
ctl->server.authenticate = A_MSN;
+ else if (strcmp(optarg, "oauthbearer") == 0)
+ ctl->server.authenticate = A_OAUTHBEARER;
else {
fprintf(stderr,GT_("Invalid authentication `%s' specified.\n"), optarg);
errflag++;
Index: fetchmail-6.5.1/rcfile_l.l
===================================================================
--- fetchmail-6.5.1.orig/rcfile_l.l
+++ fetchmail-6.5.1/rcfile_l.l
@@ -103,6 +103,7 @@ cram(-md5)? { SETSTATE(0); yylval.proto
msn { SETSTATE(0); yylval.proto = A_MSN; return AUTHTYPE;}
ntlm { SETSTATE(0); yylval.proto = A_NTLM; return AUTHTYPE;}
<AUTH>password { SETSTATE(0); yylval.proto = A_PASSWORD; return AUTHTYPE;}
+oauthbearer { SETSTATE(0); yylval.proto = A_OAUTHBEARER; return AUTHTYPE;}
timeout { return TIMEOUT;}
idletimeout { return IDLETIMEOUT;}
envelope { return ENVELOPE; }

View File

@ -0,0 +1,314 @@
From: Matthew Ogilvie <mmogilvi+fml@zoho.com>
Date: Sun, 28 May 2017 00:01:02 -0600
Subject: add passwordfile and passwordfd options
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: cdd7182f65734c97723ba5f282040e08d830e650
---
fetchmail.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
fetchmail.h | 2 +
fetchmail.man | 40 +++++++++++++++++++++++++++-
options.c | 16 +++++++++++
rcfile_l.l | 2 +
rcfile_y.y | 6 ++++
6 files changed, 145 insertions(+), 3 deletions(-)
Index: fetchmail-6.5.1/fetchmail.c
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.c
+++ fetchmail-6.5.1/fetchmail.c
@@ -471,7 +471,7 @@ int main(int argc, char **argv)
/* Server won't care what the password is, but there
must be some non-null string here. */
ctl->password = ctl->remotename;
- else
+ else if (!ctl->passwordfile && ctl->passwordfd==-1)
{
const netrc_entry *p;
@@ -649,8 +649,81 @@ int main(int argc, char **argv)
if (ctl->active && !(implicitmode && ctl->server.skip)
&& !NO_PASSWORD(ctl) && !ctl->password)
{
- if (!isatty(0))
+ if (ctl->passwordfd != -1)
{
+ char msg[PASSWORDLEN+1];
+ char *mi;
+
+ /* Read one character at a time to avoid reading too
+ * much if more than one password sent in through this FD
+ * (although that would be a questionable practice).
+ */
+ for (mi = msg; mi<msg+sizeof(msg)-1; ++mi) {
+ int res = read(ctl->passwordfd, mi, 1);
+ if(res == -1) {
+ int saveErrno = errno;
+ fprintf(stderr,
+ GT_("fetchmail: unable to read password from fd=%d: %s\n"),
+ ctl->passwordfd,
+ strerror(saveErrno));
+ memset(msg, 0x55, mi-msg);
+ return PS_AUTHFAIL;
+ }
+ if (res == 0 || *mi == '\n')
+ break;
+ }
+ *mi = '\0';
+ if (mi == msg) {
+ fprintf(stderr,
+ GT_("fetchmail: empty password read from fd=%d\n"),
+ ctl->passwordfd);
+ return PS_AUTHFAIL;
+ }
+
+ ctl->password = xstrdup(msg);
+ memset(msg, 0x55, mi-msg);
+ } else if (ctl->passwordfile) {
+ int fd = open(ctl->passwordfile, O_RDONLY);
+ char msg[PASSWORDLEN+1];
+ char *newline;
+ int res;
+
+ if (fd == -1) {
+ int saveErrno = errno;
+ fprintf(stderr,
+ GT_("fetchmail: unable to open %s: %s\n"),
+ ctl->passwordfile,
+ strerror(saveErrno));
+ return PS_AUTHFAIL;
+ }
+
+ res = read(fd, msg, sizeof(msg)-1);
+ if (res == -1 || close(fd) == -1) {
+ int saveErrno = errno;
+ fprintf(stderr,
+ GT_("fetchmail: error reading %s: %s\n"),
+ ctl->passwordfile,
+ strerror(saveErrno));
+ return PS_AUTHFAIL;
+ }
+ msg[res] = '\0';
+
+ newline = memchr(msg, '\n', res);
+ if (newline != NULL) {
+ *newline = '\0';
+ }
+
+ if (strlen(msg) == 0) {
+ fprintf(stderr,
+ GT_("fetchmail: empty password read from %s\n"),
+ ctl->passwordfile);
+ memset(msg, 0x55, res);
+ return PS_AUTHFAIL;
+ }
+
+ ctl->password = xstrdup(msg);
+ memset(msg, 0x55, res);
+ } else if (!isatty(0)) {
fprintf(stderr,
GT_("fetchmail: can't find a password for %s@%s.\n"),
ctl->remotename, ctl->server.pollname);
@@ -1046,6 +1119,10 @@ static void optmerge(struct query *h2, s
FLAG_MERGE(wildcard);
STRING_MERGE(remotename);
STRING_MERGE(password);
+ FLAG_MERGE(passwordfile);
+ if (force ? h1->passwordfd!=-1 : h2->passwordfd==-1) {
+ h2->passwordfd = h1->passwordfd;
+ }
STRING_MERGE(mda);
STRING_MERGE(bsmtp);
FLAG_MERGE(listener);
@@ -1112,6 +1189,7 @@ static int load_params(int argc, char **
def_opts.smtp_socket = -1;
def_opts.smtpaddress = (char *)0;
def_opts.smtpname = (char *)0;
+ def_opts.passwordfd = -1;
def_opts.server.protocol = P_AUTO;
def_opts.server.timeout = CLIENT_TIMEOUT;
def_opts.server.idle_timeout = CLIENT_IDLE_TIMEOUT;
Index: fetchmail-6.5.1/fetchmail.h
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.h
+++ fetchmail-6.5.1/fetchmail.h
@@ -312,6 +312,8 @@ struct query
int wildcard; /* should unmatched names be passed through */
char *remotename; /* remote login name to use */
char *password; /* remote password to use */
+ char *passwordfile; /* filename; first line contains password */
+ int passwordfd; /* fileno that password will be piped into */
struct idlist *mailboxes; /* list of mailboxes to check */
/* per-forwarding-target data */
Index: fetchmail-6.5.1/fetchmail.man
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.man
+++ fetchmail-6.5.1/fetchmail.man
@@ -1056,6 +1056,37 @@ The default is your login name on the cl
\fBfetchmail\fP.
See USER AUTHENTICATION below for a complete description.
.TP
+.B \-\-passwordfile <filename>
+(Keyword: passwordfile)
+.br
+Specifies a file name from which to read the first line to use as the password.
+Useful if something changes the password/token often without regenerating a
+long fetchmailrc file, such as with typical xoauth2 authentication tokens.
+Protect the file with appropriate permissions to avoid leaking your password.
+Fetchmail might not re-read the file in daemon mode (-d) unless the
+fetchmailrc file also changes, so it might make sense to run it in
+non-daemon mode from some other background process (cron and/or whatever
+updates the password).
+.TP
+.B \-\-passwordfd <integer>
+(Keyword: passwordfd)
+.br
+Specifies a file descriptor number inherited from the calling process,
+from which fetchmail should read one line to use as the password.
+The descriptor will usually be the receiving end of a pipe (equivalent
+to "something | fetchmail \-\-passwordfd 5 5<\&0"),
+although it could also be a redirected input file
+(equivalent to "fetchmail \-\-passwordfd 5 5</path/to/file").
+Useful if something wants to manage password ownership more securely
+than files, or if the password/token changes often,
+such as with typical xoauth2 authentication tokens. Normal interactive
+mode passwords requires that standard input is a terminal and disables
+echo, but passwordfd does not care. Do not do something
+like "echo 'password' | fetchmail ...", since echo's arguments are
+likely to (briefly) be publicly visible in process listings.
+This probably doesn't interact well with deamon mode: when will it
+re-read a new password?
+.TP
.B \-I <specification> | \-\-interface <specification>
(Keyword: interface)
.br
@@ -1148,7 +1179,8 @@ setting also allows the non-standard "xo
the same token) if the server only claims to support "xoauth2".
External tools are necessary to obtain
such tokens. Access tokens often expire fairly quickly (e.g. 1 hour),
-and new ones need to be generated from renewal tokens. See the
+and new ones need to be generated from renewal tokens, so the
+"passwordfile", "passwordfd", or "pwmd_*" options may be useful. See the
oauth2.py script from
.URL https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough "Google's Oauth2 Run Through" ,
and other oauth2 documentation. For services like gmail, an "App Password"
@@ -2100,6 +2132,12 @@ T}
pass[word] \& \& T{
Specify remote account password
T}
+passwordfile \-\-... \& T{
+File name with password in first line.
+T}
+passwordfd \-\-... \& T{
+Inherited file descriptor from which to read one line for the password.
+T}
ssl \& \& T{
Connect to server over the specified base protocol using SSL encryption
T}
Index: fetchmail-6.5.1/options.c
===================================================================
--- fetchmail-6.5.1.orig/options.c
+++ fetchmail-6.5.1/options.c
@@ -29,6 +29,8 @@ enum {
LA_POSTMASTER,
LA_NOBOUNCE,
LA_AUTH,
+ LA_PASSWORDFILE,
+ LA_PASSWORDFD,
LA_FETCHDOMAINS,
LA_BSMTP,
LA_LMTP,
@@ -98,6 +100,8 @@ static const struct option longoptions[]
{"port", required_argument, (int *) 0, 'P' },
{"service", required_argument, (int *) 0, 'P' },
{"auth", required_argument, (int *) 0, LA_AUTH},
+ {"passwordfile", required_argument, (int *) 0, LA_PASSWORDFILE },
+ {"passwordfd", required_argument, (int *) 0, LA_PASSWORDFD },
{"timeout", required_argument, (int *) 0, 't' },
{"envelope", required_argument, (int *) 0, 'E' },
{"qvirtual", required_argument, (int *) 0, 'Q' },
@@ -231,6 +235,7 @@ int parsecmdline (int argc /** argument
memset(ctl, '\0', sizeof(struct query)); /* start clean */
ctl->smtp_socket = -1;
+ ctl->passwordfd = -1;
while (!errflag &&
(c = getopt_long(argc,argv,shortoptions,
@@ -402,6 +407,17 @@ int parsecmdline (int argc /** argument
errflag++;
}
break;
+ case LA_PASSWORDFILE:
+ ctl->passwordfile = optarg;
+ break;
+ case LA_PASSWORDFD:
+ ctl->passwordfd = xatoi(optarg, &errflag);
+ if (ctl->passwordfd < 0) {
+ fprintf(stderr,GT_("Invalid file descriptor %d\n"),
+ ctl->passwordfd);
+ errflag++;
+ }
+ break;
case 't':
ctl->server.timeout = xatoi(optarg, &errflag);
if (ctl->server.timeout == 0)
Index: fetchmail-6.5.1/rcfile_l.l
===================================================================
--- fetchmail-6.5.1.orig/rcfile_l.l
+++ fetchmail-6.5.1/rcfile_l.l
@@ -116,6 +116,8 @@ accept { return ACCEPT; }
reject { return REJECT_; }
user(name)? {SETSTATE(NAME); return USERNAME; }
+passwordfile { return PASSWORDFILE; }
+passwordfd { return PASSWORDFD; }
<INITIAL,NAME>pass(word)? {SETSTATE(NAME); return PASSWORD; }
folder(s)? { return FOLDER; }
smtp(host)? { return SMTPHOST; }
Index: fetchmail-6.5.1/rcfile_y.y
===================================================================
--- fetchmail-6.5.1.orig/rcfile_y.y
+++ fetchmail-6.5.1/rcfile_y.y
@@ -62,6 +62,7 @@ void yyerror (const char *s)
%token DEFAULTS POLL SKIP VIA AKA LOCALDOMAINS PROTOCOL
%token AUTHENTICATE TIMEOUT IDLETIMEOUT KPOP SDPS ENVELOPE QVIRTUAL
%token USERNAME PASSWORD FOLDER SMTPHOST FETCHDOMAINS MDA BSMTP LMTP
+%token PASSWORDFILE PASSWORDFD
%token SMTPADDRESS SMTPNAME SPAMRESPONSE PRECONNECT POSTCONNECT LIMIT WARNINGS
%token INTERFACE MONITOR PLUGIN PLUGOUT
%token IS HERE THERE TO MAP
@@ -314,6 +315,8 @@ user_option : TO mapping_list HERE
| IS STRING THERE {current.remotename = $2;}
| PASSWORD STRING {current.password = $2;}
+ | PASSWORDFILE STRING {current.passwordfile = $2;}
+ | PASSWORDFD NUMBER {current.passwordfd = NUM_VALUE_IN($2);}
| FOLDER folder_list
| SMTPHOST smtp_list
| FETCHDOMAINS fetch_list
@@ -495,6 +498,7 @@ static void reset_server(const char *nam
trailer = FALSE;
memset(&current,'\0',sizeof(current));
current.smtp_socket = -1;
+ current.passwordfd = -1;
current.server.pollname = xstrdup(name);
current.server.skip = skip;
}
@@ -515,6 +519,7 @@ static void user_reset(void)
memset(&current, '\0', sizeof(current));
current.smtp_socket = -1;
+ current.passwordfd = -1;
current.server = save;
}
@@ -535,6 +540,7 @@ struct query *hostalloc(struct query *in
{
memset(node, '\0', sizeof(struct query));
node->smtp_socket = -1;
+ node->passwordfd = -1;
}
/* append to end of list */

View File

@ -0,0 +1,41 @@
From: Matthew Ogilvie <mmogilvi+fml@zoho.com>
Date: Fri, 21 Dec 2018 09:00:46 -0700
Subject: add query_to64_outsize() utility function
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: cc6e146d516140df800da68976eb7c0aa1cef7c0
---
base64.c | 7 +++++++
fetchmail.h | 1 +
2 files changed, 8 insertions(+)
Index: fetchmail-6.5.1/base64.c
===================================================================
--- fetchmail-6.5.1.orig/base64.c
+++ fetchmail-6.5.1/base64.c
@@ -66,6 +66,13 @@ fail:
return rc;
}
+size_t query_to64_outsize(size_t inlen)
+/* Returns how much space needs to be allocated to receive the output from
+ * to64frombits(), including the '\0' terminator. */
+{
+ return ((inlen+2)/3)*4+1;
+}
+
int from64tobits(void *out_, const char *in, int maxlen)
/* base 64 to raw bytes in quasi-big-endian order, returning count of bytes */
/* maxlen limits output buffer size, set to zero to ignore */
Index: fetchmail-6.5.1/fetchmail.h
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.h
+++ fetchmail-6.5.1/fetchmail.h
@@ -611,6 +611,7 @@ int prc_filecheck(const char *, const fl
/* base64.c */
unsigned len64frombits(unsigned inlen); /** calculate length needed to encode inlen octets. warnings: 1. caller needs to add 1 for a trailing \0 byte himself. 2. returns 0 for inlen 0! */
int to64frombits(char *, const void *, int inlen, size_t outlen);
+size_t query_to64_outsize(size_t inlen);
int from64tobits(void *, const char *, int mxoutlen);
/* unmime.c */

View File

@ -0,0 +1,160 @@
From: William Bader <william@newspapersystems.com>
Date: Sun, 31 Jan 2021 06:42:46 +0000
Subject: Add README.OAUTH2 issue #27
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: d52ba9652c9207358e0b9acc11403688f6f16b9e
---
README.OAUTH2 | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 147 insertions(+)
--- /dev/null
+++ b/README.OAUTH2
@@ -0,0 +1,147 @@
+OAUTH2 support for gmail
+========================
+
+Preface
+-------
+
+fetchmail 7 adds support for OAuth2.
+You create a project in google that requests gmail access to request an OAuth2 client id and client secret.
+Then you use the contributed fetchmail-oauth2.py to request a refresh token for gmail access to your gmail account.
+Then you use the fetchmail-oauth2.py again to request temporary access tokens that fetchmail uses like a password.
+
+Create a Google project and request an OAuth2 client id and client secret
+-------------------------------------------------------------------------
+
+* Open the Google API Dashboard: https://console.developers.google.com/apis/dashboard
+* The first time that you enter the page, you will have to select your country and agree to terms of service.
+* You should see a title bar with "Google APIs" and a menu down the left with "Dashboard, Library,
+ Credentials, OAuth consent screen, Domain verification, Page usage agreements".
+* Click to create a new project, possibly on a pull-down arrow to the right of "Google APIs" on the title bar.
+* Click on "NEW PROJECT".
+ + Enter a project name like "fetchmail".
+ + You can leave "Location" as "No organization" for personal email.
+ + If you are a G Suite administrator, you might be able to enter your G Suite organization.
+ + When you enter the project name, you will get a message like "Project ID: fetchmail-123456. It cannot be changed later."
+ + Make a note of the full project name.
+ + Click on "CREATE".
+ + Google will take a few seconds to create the project.
+* Switch to the new project, either from "SELECT PROJECT" in the notification window or on the title bar pulldown after "Google APIs".
+* Click on "Credentials" on the menu at the left.
+* Click on "+ CREATE CREDENTIALS" at the top of the window.
+* Select "OAuth client ID" from the list of credential types.
+* Click on "CONFIGURE CONSENT SCREEN" at the right.
+ + Select "External" from the list of user types. "Internal" is for organizations with G Suite.
+ + Click "CREATE".
+* Fill out the app registration form.
+ + "App name" can be the full project name, like "fetchmail-123456". It has to be unique.
+ + "User support email" can be your gmail email.
+ + "App logo" can be empty. I used /usr/share/icons/Adwaita/256x256/legacy/emblem-mail.png
+ + "Application home page" can be empty.
+ + "Application privacy policy link" can be empty.
+ + "Application terms of service link" can be empty.
+ + "Authorized domain" can be empty.
+ + "Developer contact email address" can be your gmail email.
+ + Click "SAVE AND CONTINUE".
+ + Click "ADD OR REMOVE SCOPES" on the "Edit app registration" screen.
+ + Click on "Google API Library". This opens a new tab.
+ + Filter for "Email" and click on "Gmail API".
+ + Click on "ENABLE".
+ + Return to the "Edit app registration" tab and refresh.
+ + Click "ADD OR REMOVE SCOPES" on the "Edit app registration" screen.
+ + I think that the scope ".../auth/gmail.modify" to "View and modify but not delete your email" is sufficient.
+ + Click on "SAVE AND CONTINUE". This opens the "Test Users" window.
+ + Click on "+ ADD USERS".
+ + Enter you gmail address and click on "ADD".
+ + Click on "SAVE AND CONTINUE".
+ + This opens a "Summary" page.
+ + If you need to change something, click on "OAuth consent screen" on the menu at the left and then "EDIT APP" to step through the screens again.
+* Click on "Credentials" on the menu at the left to create client credentials.
+ + Click on "+ CREATE CREDENTIALS" at the top of the window.
+ + Select "OAuth client ID" from the list of credential types.
+ + Select "Desktop app" from the list of "Application types".
+ + "Name" can be "DesktopClient1" or whatever the screen suggests.
+ + Click on "CREATE".
+ + It will show a window with "Your Client ID" and "Your Client Secret". Copy them somewhere safe.
+
+Download and build fetchmail 7
+------------------------------
+```
+git clone https://gitlab.com/fetchmail/fetchmail.git
+cd fetchmail
+git checkout -t origin/next
+./autogen.sh
+./configure
+make
+make check
+sudo make install
+```
+
+Configure fetchmail-oauth2.py
+-----------------------------
+* Create a file, for example /home/yourname/.fetchmail-oauth2
+```
+client_id=YOUR-CLIENT-ID
+client_secret=YOUR-CLIENT-SECRET
+refresh_token_file=/home/yourname/.fetchmail-refresh
+access_token_file=/home/yourname/.fetchmail-token
+max_age_sec=3000
+```
+* Replace YOUR-CLIENT-ID and YOUR-CLIENT-SECRET with the keys for "Your Client ID" and "Your Client Secret" from the previous step.
+* The refresh and token files do not need to exist, but they have to be valid paths.
+* Run `contrib/fetchmail-oauth2.py -c /home/yourname/.fetchmail-oauth2 --obtain_refresh_token_file`
+ + The script will give you a URL.
+ + Paste the URL into a web browser.
+ + URL should open a google authentication page.
+ + Select the email account.
+ + Google will warn that the app isn't verified. Click on "Continue".
+ + Google will warn that "fetchmail-123456 wants to access your Google Account `your.name@gmail.com`".
+ + Click on "Allow".
+ + The page will display the sign in key.
+ + Paste the key into the script.
+ + The script will report:
+```
+Refresh token saved to '/home/yourname/.fetchmail-refresh'
+Initial access token saved to '/home/yourname/.fetchmail-token'
+Access Token Expiration Seconds: 3599
+```
+ + Hopefully you will not need to do this again for months or years.
+* Run `chmod 0600` on all of the files .fetchmail-oauth2 .fetchmail-refresh .fetchmail-token
+
+Configure fetchmail
+---------------------
+* Create an entry in your `.fetchmailrc`
+```
+poll imap.gmail.com protocol imap
+ auth oauthbearer username "your.name@gmail.com"
+ passwordfile "/home/yourname/.fetchmail-token"
+ is yourname here
+ fetchlimit 10 folder "Download"
+ keep
+ sslmode wrapped sslcertck
+```
+* Run `chmod 0400` on your `.fetchmailrc`
+* The optional "fetchlimit #" limits the number of emails if you are testing.
+* The optional "folder name" sets the folder to check.
+* I made gmail filters that add a "Download" label to important emails.
+* Fetchmail downloads unread emails. You can go into gmail and mark a few emails unread for testing.
+* Try running fetchmail once at a command line.
+
+Script fetchmail
+----------------
+* Each access token expires after an hour.
+* If you run fetchmail from cron, you should run `fetchmail-oauth2.py -c /home/yourname/.fetchmail-oauth2 --auto_refresh ; fetchmail`
+* For example, `*/2 * * * * (fetchmail-oauth2.py -c /home/yourname/.fetchmail-oauth2 --auto_refresh ; fetchmail) > /home/yourname/fetchmail.log 2>&1`
+* The `--auto_refresh` option checks the age of the key against the `max_age_sec` and renews it if necessary.
+* `max_age_sec=3000` in `.fetchmail-oauth2` renews the key after 50 minutes, which should give a safe margin.
+
+Further reading
+---------------
+* Instructions by the author of OAuth2 support for Fetchmail and Postfix
+ + Setting Up OAUTH2 Support for Fetchmail and Postfix http://mmogilvi.users.sourceforge.net/software/oauthbearer.html
+ + Run `fetchmail-oauth2.py --help | less`
+* Documents from Google
+ + Using OAuth 2.0 to Access Google APIs https://developers.google.com/identity/protocols/oauth2
+ + Integrating Google Sign-In into your web app https://developers.google.com/identity/sign-in/web/devconsole-project
+* Google links
+ + Google API Dashboard: https://console.developers.google.com/apis/dashboard
+ + Google Cloud Resource Manager: https://console.developers.google.com/cloud-resource-manager

View File

@ -0,0 +1,23 @@
From: Matthias Andree <matthias.andree@gmx.de>
Date: Sat, 24 Apr 2021 15:12:01 +0200
Subject: Bump max. passwordlen to 10000 bytes.
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: 919fd787540c4a3fa4694566edce406df1e42001
---
fetchmail.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Index: fetchmail-6.5.1/fetchmail.h
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.h
+++ fetchmail-6.5.1/fetchmail.h
@@ -101,7 +101,7 @@ struct addrinfo;
#define NAMELEN 64 /* max username length */
/* oauth2 access tokens seem to be about 130 characters; make this longer: */
-#define PASSWORDLEN 4096 /* max password length; oauth2 tokens have no maximum length */
+#define PASSWORDLEN 10000 /* max password length; oauth2 tokens have no maximum length */
#define DIGESTLEN 33 /* length of MD5 digest */
/* exit code values */

View File

@ -0,0 +1,29 @@
From: Matthias Andree <matthias.andree@gmx.de>
Date: Sun, 25 Nov 2018 12:09:07 +0100
Subject: Chase and integrate interface change.
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: d52b7b6859d46134b46e6de9b408739b18745d47
---
oauth2.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
--- a/oauth2.c
+++ b/oauth2.c
@@ -10,6 +10,7 @@
#include "oauth2.h"
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
char *get_oauth2_string(struct query *ctl,flag xoauth2)
@@ -52,7 +53,7 @@ char *get_oauth2_string(struct query *ct
}
oauth2b64 = (char *)xmalloc(2*strlen(oauth2str)+8);
- to64frombits(oauth2b64, oauth2str, strlen(oauth2str));
+ to64frombits(oauth2b64, oauth2str, strlen(oauth2str), oauth2len);
memset(oauth2str, 0x55, strlen(oauth2str));
free(oauth2str);

View File

@ -0,0 +1,45 @@
From: Matthew Ogilvie <mmogilvi+fml@zoho.com>
Date: Fri, 9 Jun 2017 19:31:17 -0600
Subject: give each ctl it's own copy of password
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: 469b0a212e7f047ab16ef46a9158df5fb373e8c2
pwdb_* and passwordfile options may free and re-allocate password
for each poll operation. Giving each context it's own copy of
the password should prevent accessing freed memory in another copy.
I haven't tested pwmd, but these seem like obvious fixes.
---
fetchmail.c | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
Index: fetchmail-6.5.1/fetchmail.c
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.c
+++ fetchmail-6.5.1/fetchmail.c
@@ -470,7 +470,7 @@ int main(int argc, char **argv)
if (NO_PASSWORD(ctl))
/* Server won't care what the password is, but there
must be some non-null string here. */
- ctl->password = ctl->remotename;
+ ctl->password = xstrdup(ctl->remotename);
else if (!ctl->passwordfile && ctl->passwordfd==-1)
{
const netrc_entry *p;
@@ -1118,7 +1118,15 @@ static void optmerge(struct query *h2, s
FLAG_MERGE(wildcard);
STRING_MERGE(remotename);
- STRING_MERGE(password);
+ if (force ? !!h1->password : !h2->password) {
+ if (h2->password) {
+ memset(h2->password, 0x55, strlen(h2->password));
+ xfree(h2->password);
+ }
+ if (h1->password) {
+ h2->password = xstrdup(h1->password);
+ }
+ }
FLAG_MERGE(passwordfile);
if (force ? h1->passwordfd!=-1 : h2->passwordfd==-1) {
h2->passwordfd = h1->passwordfd;

View File

@ -0,0 +1,32 @@
From: =?utf-8?q?Martin_Sj=C3=B6lund_=3Cmartin=40sjoelund=2Ese=3E?=
Date: Thu, 17 Dec 2020 09:09:44 +0100
Subject: Increase max password length to handle oauth tokens
Git-repo: https://gitlab.com/fetchmail/fetchmail.git
Git-commit: 6e877b5d92527ad501aaef46e37704b51db316fb
The maximum length of oauth2 access tokens is unbounded.
* Google uses 2048 byte access tokens and "Google reserves the right
to change token size within these limits, and your application
must support variable token sizes accordingly."
* My Office365 access token is 2108 bytes long.
* Intuit says you must support 4096 byte access tokens.
This simply patches the hard-coded limit to 4096 bytes, but it might
not be sufficient.
---
fetchmail.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Index: fetchmail-6.5.1/fetchmail.h
===================================================================
--- fetchmail-6.5.1.orig/fetchmail.h
+++ fetchmail-6.5.1/fetchmail.h