Mercurial > p > roundup > code
comparison roundup/scripts/roundup_mailgw.py @ 7064:3359dc1dabb0
Add OAuth authentication to the mailgw script
Now IMAPS can be used with OAuth as required by several large cloud
providers. Move command line processing of the mailgw script to
``argparse``. Note that the command line options of the mailgw have
changed, see upgrading.txt for details.
| author | Ralf Schlatterbeck <rsc@runtux.com> |
|---|---|
| date | Wed, 23 Nov 2022 10:13:48 +0100 |
| parents | c7a9f9c1801d |
| children | 27c2d7295ba2 |
comparison
equal
deleted
inserted
replaced
| 7063:aca710a3b687 | 7064:3359dc1dabb0 |
|---|---|
| 21 | 21 |
| 22 | 22 |
| 23 # --- patch sys.path to make sure 'import roundup' finds correct version | 23 # --- patch sys.path to make sure 'import roundup' finds correct version |
| 24 import sys | 24 import sys |
| 25 import os.path as osp | 25 import os.path as osp |
| 26 from argparse import ArgumentParser, RawDescriptionHelpFormatter | |
| 26 | 27 |
| 27 thisdir = osp.dirname(osp.abspath(__file__)) | 28 thisdir = osp.dirname(osp.abspath(__file__)) |
| 28 rootdir = osp.dirname(osp.dirname(thisdir)) | 29 rootdir = osp.dirname(osp.dirname(thisdir)) |
| 29 if (osp.exists(thisdir + '/__init__.py') and | 30 if (osp.exists(thisdir + '/__init__.py') and |
| 30 osp.exists(rootdir + '/roundup/__init__.py')): | 31 osp.exists(rootdir + '/roundup/__init__.py')): |
| 41 | 42 |
| 42 from roundup import mailgw | 43 from roundup import mailgw |
| 43 from roundup.i18n import _ | 44 from roundup.i18n import _ |
| 44 | 45 |
| 45 | 46 |
| 46 def usage(args, message=None): | 47 usage_epilog = """ |
| 47 if message is not None: | |
| 48 print(message) | |
| 49 print(_( | |
| 50 """Usage: %(program)s [-v] [-c class] [[-C class] -S field=value]* [instance home] [mail source [specification]] | |
| 51 | |
| 52 Options: | |
| 53 -v: print version and exit | |
| 54 -c: default class of item to create (else the tracker's MAIL_DEFAULT_CLASS) | |
| 55 -C / -S: see below | |
| 56 | |
| 57 The roundup mail gateway may be called in one of the following ways: | 48 The roundup mail gateway may be called in one of the following ways: |
| 58 . without arguments. Then the env var ROUNDUP_INSTANCE will be tried. | 49 . without arguments. Then the env var ROUNDUP_INSTANCE will be tried. |
| 59 . with an instance home as the only argument, | 50 . with an instance home as the only argument, |
| 60 . with both an instance home and a mail spool file, | 51 . with both an instance home and a mail spool file, or |
| 61 . with an instance home, a mail source type and its specification. | 52 . with an instance home, a mail source type and its specification. |
| 62 | 53 |
| 63 It also supports optional -C and -S arguments that allows you to set a | 54 It also supports optional -S (or --set-value) arguments that allows you |
| 64 fields for a class created by the roundup-mailgw. The default class if | 55 to set fields for a class created by the roundup-mailgw. The format for |
| 65 not specified is msg, but the other classes: issue, file, user can | 56 this option is [class.]property=value where class can be omitted and |
| 66 also be used. The -S or --set options uses the same | 57 defaults to msg. The -S options uses the same |
| 67 property=value[;property=value] notation accepted by the command line | 58 property=value[;property=value] notation accepted by the command line |
| 68 roundup command or the commands that can be given on the Subject line | 59 roundup command or the commands that can be given on the Subject line of |
| 69 of an email message. | 60 an email message (if you're using multiple properties delimited with a |
| 70 | 61 semicolon the class must be specified only once in the beginning). |
| 71 It can let you set the type of the message on a per email address basis. | 62 |
| 63 It can let you set the type of the message on a per email address basis | |
| 64 by calling roundup-mailgw with different email addresses and other | |
| 65 settings. | |
| 72 | 66 |
| 73 PIPE: | 67 PIPE: |
| 74 If there is no mail source specified, | 68 If there is no mail source specified, the mail gateway reads a single |
| 75 the mail gateway reads a single message from the standard input | 69 message from the standard input and submits the message to the |
| 76 and submits the message to the roundup.mailgw module. | 70 roundup.mailgw module. |
| 77 | 71 |
| 78 Mail source "mailbox": | 72 Mail source "mailbox": |
| 79 In this case, the gateway reads all messages from the UNIX mail spool | 73 In this case, the gateway reads all messages from the UNIX mail spool |
| 80 file and submits each in turn to the roundup.mailgw module. The file is | 74 file and submits each in turn to the roundup.mailgw module. The file is |
| 81 emptied once all messages have been successfully handled. The file is | 75 emptied once all messages have been successfully handled. The file is |
| 82 specified as: | 76 specified as: |
| 83 mailbox /path/to/mailbox | 77 mailbox /path/to/mailbox |
| 84 | 78 |
| 85 In all of the following mail source type the username and password | 79 In all of the following mail source types, the username and password |
| 86 can be stored in a ~/.netrc file. If done so case only the server name | 80 can be stored in a ~/.netrc file. If done so, only the server name |
| 87 need to be specified on the command-line. | 81 needs to be specified on the command-line. |
| 88 | 82 |
| 89 The username and/or password will be prompted for if not supplied on | 83 The username and/or password will be prompted for if not supplied on |
| 90 the command-line or in ~/.netrc. | 84 the command-line or in ~/.netrc. |
| 91 | 85 |
| 92 POP: | 86 POP: |
| 93 For the mail source "pop", the gateway reads all messages from the POP server | 87 For the mail source "pop", the gateway reads all messages from the POP |
| 94 specified and submits each in turn to the roundup.mailgw module. The | 88 server specified and submits each in turn to the roundup.mailgw module. |
| 95 server is specified as: | 89 The server is specified as: |
| 96 pop username:password@server | 90 pop username:password@server |
| 97 Alternatively, one can omit one or both of username and password: | 91 The username and password may be omitted: |
| 98 pop username@server | 92 pop username@server |
| 99 pop server | 93 pop server |
| 100 are both valid. | 94 are both valid. |
| 101 | 95 |
| 102 POPS: | 96 POPS: |
| 103 Connect to a POP server over ssl. This requires python 2.4 or later. | 97 Connect to a POP server over ssl. |
| 104 This supports the same notation as POP. | 98 This supports the same notation as POP. |
| 105 | 99 |
| 106 APOP: | 100 APOP: |
| 107 Same as POP, but using Authenticated POP: | 101 Same as POP, but using Authenticated POP: |
| 108 apop username:password@server | 102 apop username:password@server |
| 123 IMAPS_CRAM: | 117 IMAPS_CRAM: |
| 124 Connect to an IMAP server over ssl using CRAM-MD5 authentication. | 118 Connect to an IMAP server over ssl using CRAM-MD5 authentication. |
| 125 This supports the same notation as IMAP. | 119 This supports the same notation as IMAP. |
| 126 imaps_cram username:password@server [mailbox] | 120 imaps_cram username:password@server [mailbox] |
| 127 | 121 |
| 128 """) % {'program': args[0]}) | 122 IMAPS_OAUTH: |
| 129 return 1 | 123 Connect to an IMAP server over ssl using OAUTH authentication. |
| 130 | 124 Note that this does not support a password in imaps URLs. |
| 125 Instead it uses only the user and server and a command-line option for | |
| 126 the directory with the files 'access_token', 'refresh_token', and | |
| 127 'client_secret'. | |
| 128 By default this directory is 'oauth' in your tracker home directory. The | |
| 129 access token is tried first and, if expired, the refresh token together | |
| 130 with the client secret is used to retrieve a new access token. Note that | |
| 131 both token files need to be *writeable*, the access token is | |
| 132 continuously replaced and some cloud providers may also renew the | |
| 133 refresh token from time to time: | |
| 134 imaps_oauth username@server [mailbox] | |
| 135 Note that you also have to specify the OAuth client id with the | |
| 136 ``--oauth-client-id`` option on the command line. The refresh and | |
| 137 access tokens (the latter can be left empty) and the client secret need | |
| 138 to be retrieved via cloud provider specific protocols or websites. | |
| 139 | |
| 140 | |
| 141 | |
| 142 """ | |
| 143 | |
| 144 def parse_arguments(argv): | |
| 145 '''Handle the arguments to the program | |
| 146 ''' | |
| 147 # take the argv array and parse it leaving the non-option | |
| 148 # arguments in the list 'args'. | |
| 149 cmd = ArgumentParser(epilog=usage_epilog, | |
| 150 formatter_class=RawDescriptionHelpFormatter) | |
| 151 cmd.add_argument('args', nargs='*') | |
| 152 cmd.add_argument('-v', '--version', action='store_true', | |
| 153 help='print version and exit') | |
| 154 cmd.add_argument('-c', '--default_class', default='', | |
| 155 help="Default class of item to create (else the tracker's " | |
| 156 "MAILGW_DEFAULT_CLASS)") | |
| 157 cmd.add_argument('-I', '--oauth-client-id', | |
| 158 help='ID for OAUTH token refresh') | |
| 159 cmd.add_argument('-O', '--oauth-directory', | |
| 160 help='Directory with OAUTH credentials, default "oauth" in ' | |
| 161 'tracker home') | |
| 162 cmd.add_argument('-S', '--set-value', action='append', | |
| 163 help="Set additional properties on some classes", default=[]) | |
| 164 cmd.add_argument('-T', '--oauth-token-endpoint', | |
| 165 help="OAUTH token endpoint for access_token renew, default=%(default)s", | |
| 166 default='https://login.microsoftonline.com/' | |
| 167 'organizations/oauth2/v2.0/token') | |
| 168 return cmd, cmd.parse_args(argv) | |
| 131 | 169 |
| 132 def main(argv): | 170 def main(argv): |
| 133 '''Handle the arguments to the program and initialise environment. | 171 '''Handle the arguments to the program and initialise environment. |
| 134 ''' | 172 ''' |
| 135 # take the argv array and parse it leaving the non-option | 173 cmd, args = parse_arguments(argv) |
| 136 # arguments in the args array. | 174 if args.version: |
| 137 try: | 175 print('%s (python %s)' % (roundup_version, sys.version.split()[0])) |
| 138 optionsList, args = getopt.getopt(argv[1:], 'vc:C:S:', ['set=', | 176 return |
| 139 'class=']) | |
| 140 except getopt.GetoptError: | |
| 141 # print help information and exit: | |
| 142 usage(argv) | |
| 143 sys.exit(2) | |
| 144 | |
| 145 for (opt, _arg) in optionsList: | |
| 146 if opt == '-v': | |
| 147 print('%s (python %s)' % (roundup_version, sys.version.split()[0])) | |
| 148 return | |
| 149 | 177 |
| 150 # figure the instance home | 178 # figure the instance home |
| 151 if len(args) > 0: | 179 if len(args.args) > 0: |
| 152 instance_home = args[0] | 180 instance_home = args.args[0] |
| 153 else: | 181 else: |
| 154 instance_home = os.environ.get('ROUNDUP_INSTANCE', '') | 182 instance_home = os.environ.get('ROUNDUP_INSTANCE', '') |
| 155 if not (instance_home and os.path.isdir(instance_home)): | 183 if not (instance_home and os.path.isdir(instance_home)): |
| 156 return usage(argv) | 184 cmd.print_help(sys.stderr) |
| 185 return _('\nError: The instance home must be specified') | |
| 157 | 186 |
| 158 # get the instance | 187 # get the instance |
| 159 import roundup.instance | 188 import roundup.instance |
| 160 instance = roundup.instance.open(instance_home) | 189 instance = roundup.instance.open(instance_home) |
| 161 | 190 |
| 162 if hasattr(instance, 'MailGW'): | 191 if hasattr(instance, 'MailGW'): |
| 163 handler = instance.MailGW(instance, optionsList) | 192 handler = instance.MailGW(instance, args) |
| 164 else: | 193 else: |
| 165 handler = mailgw.MailGW(instance, optionsList) | 194 handler = mailgw.MailGW(instance, args) |
| 166 | 195 |
| 167 # if there's no more arguments, read a single message from stdin | 196 # if there's no more arguments, read a single message from stdin |
| 168 if len(args) == 1: | 197 if len(args.args) == 1: |
| 169 return handler.do_pipe() | 198 return handler.do_pipe() |
| 170 | 199 |
| 171 # otherwise, figure what sort of mail source to handle | 200 # otherwise, figure what sort of mail source to handle |
| 172 if len(args) < 3: | 201 if len(args.args) < 3: |
| 173 return usage(argv, _( | 202 cmd.print_help(sys.stderr) |
| 174 'Error: not enough source specification information')) | 203 return _('\nError: not enough source specification information') |
| 175 source, specification = args[1:3] | 204 |
| 205 source, specification = args.args[1:3] | |
| 176 | 206 |
| 177 # time out net connections after a minute if we can | 207 # time out net connections after a minute if we can |
| 178 if source not in ('mailbox', 'imaps', 'imaps_cram'): | 208 if source not in ('mailbox', 'imaps', 'imaps_cram', 'imaps_oauth'): |
| 179 if hasattr(socket, 'setdefaulttimeout'): | 209 if hasattr(socket, 'setdefaulttimeout'): |
| 180 socket.setdefaulttimeout(60) | 210 socket.setdefaulttimeout(60) |
| 181 | 211 |
| 182 if source == 'mailbox': | 212 if source == 'mailbox': |
| 183 return handler.do_mailbox(specification) | 213 return handler.do_mailbox(specification) |
| 207 ssl = source.endswith('s') | 237 ssl = source.endswith('s') |
| 208 return handler.do_pop(server, username, password, ssl) | 238 return handler.do_pop(server, username, password, ssl) |
| 209 elif source == 'apop': | 239 elif source == 'apop': |
| 210 return handler.do_apop(server, username, password) | 240 return handler.do_apop(server, username, password) |
| 211 elif source.startswith('imap'): | 241 elif source.startswith('imap'): |
| 212 ssl = cram = 0 | 242 d = {} |
| 213 if source.endswith('s'): | 243 if source.endswith('s'): |
| 214 ssl = 1 | 244 d.update(ssl = 1) |
| 215 elif source.endswith('s_cram'): | 245 elif source.endswith('s_cram'): |
| 216 ssl = cram = 1 | 246 d.update(ssl = 1, cram = 1) |
| 247 elif source == 'imaps_oauth': | |
| 248 d.update(ssl = 1, oauth = 1, oauth_path = args.oauth_directory) | |
| 249 d.update(token_endpoint = args.oauth_token_endpoint) | |
| 250 d.update(oauth_client_id = args.oauth_client_id) | |
| 217 mailbox = '' | 251 mailbox = '' |
| 218 if len(args) > 3: | 252 if len(args.args) > 3: |
| 219 mailbox = args[3] | 253 mailbox = args.args[3] |
| 220 return handler.do_imap(server, username, password, mailbox, ssl, | 254 return handler.do_imap(server, username, password, mailbox, **d) |
| 221 cram) | |
| 222 | 255 |
| 223 return usage(argv, _('Error: The source must be either "mailbox",' | 256 return usage(argv, _('Error: The source must be either "mailbox",' |
| 224 ' "pop", "pops", "apop", "imap", "imaps" or' | 257 ' "pop", "pops", "apop", "imap", "imaps" or' |
| 225 ' "imaps_cram')) | 258 ' "imaps_cram')) |
| 226 | 259 |
| 227 | 260 |
| 228 def run(): | 261 def run(): |
| 229 sys.exit(main(sys.argv)) | 262 sys.exit(main(sys.argv [1:])) |
| 230 | 263 |
| 231 | 264 |
| 232 # call main | 265 # call main |
| 233 if __name__ == '__main__': | 266 if __name__ == '__main__': |
| 234 run() | 267 run() |
