7 |
7 |
8 VirtualMailManager's cli subcommands. |
8 VirtualMailManager's cli subcommands. |
9 """ |
9 """ |
10 |
10 |
11 import locale |
11 import locale |
12 import os |
12 import platform |
13 |
13 |
|
14 from argparse import Action, ArgumentParser, ArgumentTypeError, \ |
|
15 RawDescriptionHelpFormatter |
14 from textwrap import TextWrapper |
16 from textwrap import TextWrapper |
15 from time import strftime, strptime |
17 from time import strftime, strptime |
16 |
18 |
17 from VirtualMailManager import ENCODING |
19 from VirtualMailManager import ENCODING |
18 from VirtualMailManager.cli import get_winsize, prog, w_err, w_std |
20 from VirtualMailManager.cli import get_winsize, w_err, w_std |
19 from VirtualMailManager.cli.clihelp import help_msgs |
|
20 from VirtualMailManager.common import human_size, size_in_bytes, \ |
21 from VirtualMailManager.common import human_size, size_in_bytes, \ |
21 version_str, format_domain_default |
22 version_str, format_domain_default |
22 from VirtualMailManager.constants import __copyright__, __date__, \ |
23 from VirtualMailManager.constants import __copyright__, __date__, \ |
23 __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \ |
24 __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \ |
24 DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, EX_MISSING_ARGS, \ |
25 DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, RELOCATED_EXISTS, TYPE_ACCOUNT, \ |
25 RELOCATED_EXISTS, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED |
26 TYPE_ALIAS, TYPE_RELOCATED |
26 from VirtualMailManager.errors import VMMError |
27 from VirtualMailManager.errors import VMMError |
27 from VirtualMailManager.password import list_schemes |
28 from VirtualMailManager.password import list_schemes |
28 from VirtualMailManager.serviceset import SERVICES |
29 from VirtualMailManager.serviceset import SERVICES |
29 |
30 |
30 __all__ = ( |
31 __all__ = ( |
31 'Command', 'RunContext', 'cmd_map', 'usage', 'alias_add', 'alias_delete', |
32 'RunContext', 'alias_add', 'alias_delete', 'alias_info', 'aliasdomain_add', |
32 'alias_info', 'aliasdomain_add', 'aliasdomain_delete', 'aliasdomain_info', |
33 'aliasdomain_delete', 'aliasdomain_info', 'aliasdomain_switch', |
33 'aliasdomain_switch', 'catchall_add', 'catchall_info', 'catchall_delete', |
34 'catchall_add', 'catchall_delete', 'catchall_info', 'config_get', |
34 'config_get', 'config_set', 'configure', |
35 'config_set', 'configure', 'domain_add', 'domain_delete', 'domain_info', |
35 'domain_add', 'domain_delete', 'domain_info', 'domain_quota', |
36 'domain_note', 'domain_quota', 'domain_services', 'domain_transport', |
36 'domain_services', 'domain_transport', 'domain_note', 'get_user', 'help_', |
37 'get_user', 'list_addresses', 'list_aliases', 'list_domains', |
37 'list_domains', 'list_pwschemes', 'list_users', 'list_aliases', |
38 'list_pwschemes', 'list_relocated', 'list_users', 'relocated_add', |
38 'list_relocated', 'list_addresses', 'relocated_add', 'relocated_delete', |
39 'relocated_delete', 'relocated_info', 'setup_parser', 'user_add', |
39 'relocated_info', 'user_add', 'user_delete', 'user_info', 'user_name', |
40 'user_delete', 'user_info', 'user_name', 'user_note', 'user_password', |
40 'user_password', 'user_quota', 'user_services', 'user_transport', |
41 'user_quota', 'user_services', 'user_transport', |
41 'user_note', 'version', |
|
42 ) |
42 ) |
43 |
43 |
|
44 WS_ROWS = get_winsize()[1] - 2 |
|
45 |
44 _ = lambda msg: msg |
46 _ = lambda msg: msg |
45 txt_wrpr = TextWrapper(width=get_winsize()[1] - 1) |
47 txt_wrpr = TextWrapper(width=WS_ROWS) |
46 cmd_map = {} |
|
47 |
|
48 |
|
49 class Command(object): |
|
50 """Container class for command information.""" |
|
51 __slots__ = ('name', 'alias', 'func', 'args', 'descr') |
|
52 FMT_HLP_USAGE = """ |
|
53 usage: %(prog)s %(name)s %(args)s |
|
54 %(prog)s %(alias)s %(args)s |
|
55 """ |
|
56 |
|
57 def __init__(self, name, alias, func, args, descr): |
|
58 """Create a new Command instance. |
|
59 |
|
60 Arguments: |
|
61 |
|
62 `name` : str |
|
63 the command name, e.g. ``addalias`` |
|
64 `alias` : str |
|
65 the command's short alias, e.g. ``aa`` |
|
66 `func` : callable |
|
67 the function to handle the command |
|
68 `args` : str |
|
69 argument placeholders, e.g. ``aliasaddress`` |
|
70 `descr` : str |
|
71 short description of the command |
|
72 """ |
|
73 self.name = name |
|
74 self.alias = alias |
|
75 self.func = func |
|
76 self.args = args |
|
77 self.descr = descr |
|
78 |
|
79 @property |
|
80 def usage(self): |
|
81 """the command's usage info.""" |
|
82 return '%s %s %s' % (prog, self.name, self.args) |
|
83 |
|
84 def help_(self): |
|
85 """Print the Command's help message to stdout.""" |
|
86 old_ii = txt_wrpr.initial_indent |
|
87 old_si = txt_wrpr.subsequent_indent |
|
88 |
|
89 txt_wrpr.subsequent_indent = (len(self.name) + 2) * ' ' |
|
90 w_std(txt_wrpr.fill('%s: %s' % (self.name, self.descr))) |
|
91 |
|
92 info = Command.FMT_HLP_USAGE % dict(alias=self.alias, args=self.args, |
|
93 name=self.name, prog=prog) |
|
94 w_std(info) |
|
95 |
|
96 txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = ' ' |
|
97 try: |
|
98 [w_std(txt_wrpr.fill(_(para)) + '\n') for para |
|
99 in help_msgs[self.name]] |
|
100 except KeyError: |
|
101 w_err(1, _("Subcommand '%s' is not yet documented." % self.name), |
|
102 'see also: vmm(1)') |
|
103 |
48 |
104 |
49 |
105 class RunContext(object): |
50 class RunContext(object): |
106 """Contains all information necessary to run a subcommand.""" |
51 """Contains all information necessary to run a subcommand.""" |
107 __slots__ = ('argc', 'args', 'cget', 'hdlr', 'scmd') |
52 __slots__ = ('args', 'cget', 'hdlr') |
108 plan_a_b = _('Plan A failed ... trying Plan B: %(subcommand)s %(object)s') |
53 plan_a_b = _('Plan A failed ... trying Plan B: %(subcommand)s %(object)s') |
109 |
54 |
110 def __init__(self, argv, handler, command): |
55 def __init__(self, args, handler): |
111 """Create a new RunContext""" |
56 """Create a new RunContext""" |
112 self.argc = len(argv) |
57 self.args = args |
113 self.args = argv[:] # will be moved to argparse |
|
114 self.cget = handler.cfg_dget |
58 self.cget = handler.cfg_dget |
115 self.hdlr = handler |
59 self.hdlr = handler |
116 self.scmd = command |
|
117 |
60 |
118 |
61 |
119 def alias_add(ctx): |
62 def alias_add(ctx): |
120 """create a new alias e-mail address""" |
63 """create a new alias e-mail address""" |
121 if ctx.argc < 3: |
64 ctx.hdlr.alias_add(ctx.args.address.lower(), *ctx.args.destination) |
122 usage(EX_MISSING_ARGS, _('Missing alias address and destination.'), |
|
123 ctx.scmd) |
|
124 elif ctx.argc < 4: |
|
125 usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd) |
|
126 ctx.hdlr.alias_add(ctx.args[2].lower(), *ctx.args[3:]) |
|
127 |
65 |
128 |
66 |
129 def alias_delete(ctx): |
67 def alias_delete(ctx): |
130 """delete the specified alias e-mail address or one of its destinations""" |
68 """delete the specified alias e-mail address or one of its destinations""" |
131 if ctx.argc < 3: |
69 destination = ctx.args.destination if ctx.args.destination else None |
132 usage(EX_MISSING_ARGS, _('Missing alias address.'), ctx.scmd) |
70 ctx.hdlr.alias_delete(ctx.args.address.lower(), destination) |
133 elif ctx.argc < 4: |
|
134 ctx.hdlr.alias_delete(ctx.args[2].lower()) |
|
135 else: |
|
136 ctx.hdlr.alias_delete(ctx.args[2].lower(), ctx.args[3:]) |
|
137 |
71 |
138 |
72 |
139 def alias_info(ctx): |
73 def alias_info(ctx): |
140 """show the destination(s) of the specified alias""" |
74 """show the destination(s) of the specified alias""" |
141 if ctx.argc < 3: |
75 address = ctx.args.address.lower() |
142 usage(EX_MISSING_ARGS, _('Missing alias address.'), ctx.scmd) |
|
143 address = ctx.args[2].lower() |
|
144 try: |
76 try: |
145 _print_aliase_info(address, ctx.hdlr.alias_info(address)) |
77 _print_aliase_info(address, ctx.hdlr.alias_info(address)) |
146 except VMMError as err: |
78 except VMMError as err: |
147 if err.code is ACCOUNT_EXISTS: |
79 if err.code is ACCOUNT_EXISTS: |
148 w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo', |
80 w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo', |
149 'object': address}) |
81 'object': address}) |
150 ctx.scmd = ctx.args[1] = 'userinfo' |
82 ctx.args.scmd = 'userinfo' |
|
83 ctx.args.details = None |
151 user_info(ctx) |
84 user_info(ctx) |
152 elif err.code is RELOCATED_EXISTS: |
85 elif err.code is RELOCATED_EXISTS: |
153 w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo', |
86 w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo', |
154 'object': address}) |
87 'object': address}) |
155 ctx.scmd = ctx.args[1] = 'relocatedinfo' |
88 ctx.args.scmd = 'relocatedinfo' |
156 relocated_info(ctx) |
89 relocated_info(ctx) |
157 else: |
90 else: |
158 raise |
91 raise |
159 |
92 |
160 |
93 |
161 def aliasdomain_add(ctx): |
94 def aliasdomain_add(ctx): |
162 """create a new alias for an existing domain""" |
95 """create a new alias for an existing domain""" |
163 if ctx.argc < 3: |
96 ctx.hdlr.aliasdomain_add(ctx.args.fqdn.lower(), |
164 usage(EX_MISSING_ARGS, _('Missing alias domain name and destination ' |
97 ctx.args.destination.lower()) |
165 'domain name.'), ctx.scmd) |
|
166 elif ctx.argc < 4: |
|
167 usage(EX_MISSING_ARGS, _('Missing destination domain name.'), |
|
168 ctx.scmd) |
|
169 ctx.hdlr.aliasdomain_add(ctx.args[2].lower(), ctx.args[3].lower()) |
|
170 |
98 |
171 |
99 |
172 def aliasdomain_delete(ctx): |
100 def aliasdomain_delete(ctx): |
173 """delete the specified alias domain""" |
101 """delete the specified alias domain""" |
174 if ctx.argc < 3: |
102 ctx.hdlr.aliasdomain_delete(ctx.args.fqdn.lower()) |
175 usage(EX_MISSING_ARGS, _('Missing alias domain name.'), ctx.scmd) |
|
176 ctx.hdlr.aliasdomain_delete(ctx.args[2].lower()) |
|
177 |
103 |
178 |
104 |
179 def aliasdomain_info(ctx): |
105 def aliasdomain_info(ctx): |
180 """show the destination of the given alias domain""" |
106 """show the destination of the given alias domain""" |
181 if ctx.argc < 3: |
107 fqdn = ctx.args.fqdn.lower() |
182 usage(EX_MISSING_ARGS, _('Missing alias domain name.'), ctx.scmd) |
|
183 try: |
108 try: |
184 _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(ctx.args[2].lower())) |
109 _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(fqdn)) |
185 except VMMError as err: |
110 except VMMError as err: |
186 if err.code is ALIASDOMAIN_ISDOMAIN: |
111 if err.code is ALIASDOMAIN_ISDOMAIN: |
187 w_err(0, ctx.plan_a_b % {'subcommand': 'domaininfo', |
112 w_err(0, ctx.plan_a_b % {'subcommand': 'domaininfo', |
188 'object': ctx.args[2].lower()}) |
113 'object': fqdn}) |
189 ctx.scmd = ctx.args[1] = 'domaininfo' |
114 ctx.args.scmd = 'domaininfo' |
190 domain_info(ctx) |
115 domain_info(ctx) |
191 else: |
116 else: |
192 raise |
117 raise |
193 |
118 |
194 |
119 |
195 def aliasdomain_switch(ctx): |
120 def aliasdomain_switch(ctx): |
196 """assign the given alias domain to an other domain""" |
121 """assign the given alias domain to an other domain""" |
197 if ctx.argc < 3: |
122 ctx.hdlr.aliasdomain_switch(ctx.args.fqdn.lower(), |
198 usage(EX_MISSING_ARGS, _('Missing alias domain name and destination ' |
123 ctx.args.destination.lower()) |
199 'domain name.'), ctx.scmd) |
|
200 elif ctx.argc < 4: |
|
201 usage(EX_MISSING_ARGS, _('Missing destination domain name.'), |
|
202 ctx.scmd) |
|
203 ctx.hdlr.aliasdomain_switch(ctx.args[2].lower(), ctx.args[3].lower()) |
|
204 |
124 |
205 |
125 |
206 def catchall_add(ctx): |
126 def catchall_add(ctx): |
207 """create a new catchall alias e-mail address""" |
127 """create a new catchall alias e-mail address""" |
208 if ctx.argc < 3: |
128 ctx.hdlr.catchall_add(ctx.args.fqdn.lower(), *ctx.args.destination) |
209 usage(EX_MISSING_ARGS, _('Missing domain and destination.'), |
|
210 ctx.scmd) |
|
211 elif ctx.argc < 4: |
|
212 usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd) |
|
213 ctx.hdlr.catchall_add(ctx.args[2].lower(), *ctx.args[3:]) |
|
214 |
129 |
215 |
130 |
216 def catchall_delete(ctx): |
131 def catchall_delete(ctx): |
217 """delete the specified destination or all of the catchall destination""" |
132 """delete the specified destination or all of the catchall destination""" |
218 if ctx.argc < 3: |
133 destination = ctx.args.destination if ctx.args.destination else None |
219 usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) |
134 ctx.hdlr.catchall_delete(ctx.args.fqdn.lower(), destination) |
220 elif ctx.argc < 4: |
|
221 ctx.hdlr.catchall_delete(ctx.args[2].lower()) |
|
222 else: |
|
223 ctx.hdlr.catchall_delete(ctx.args[2].lower(), ctx.args[3:]) |
|
224 |
135 |
225 |
136 |
226 def catchall_info(ctx): |
137 def catchall_info(ctx): |
227 """show the catchall destination(s) of the specified domain""" |
138 """show the catchall destination(s) of the specified domain""" |
228 if ctx.argc < 3: |
139 address = ctx.args.fqdn.lower() |
229 usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) |
|
230 address = ctx.args[2].lower() |
|
231 _print_catchall_info(address, ctx.hdlr.catchall_info(address)) |
140 _print_catchall_info(address, ctx.hdlr.catchall_info(address)) |
232 |
141 |
233 |
142 |
234 def config_get(ctx): |
143 def config_get(ctx): |
235 """show the actual value of the configuration option""" |
144 """show the actual value of the configuration option""" |
236 if ctx.argc < 3: |
|
237 usage(EX_MISSING_ARGS, _("Missing option name."), ctx.scmd) |
|
238 |
|
239 noop = lambda option: option |
145 noop = lambda option: option |
240 opt_formater = { |
146 opt_formater = { |
241 'misc.dovecot_version': version_str, |
147 'misc.dovecot_version': version_str, |
242 'domain.quota_bytes': human_size, |
148 'domain.quota_bytes': human_size, |
243 } |
149 } |
244 |
150 |
245 option = ctx.args[2].lower() |
151 option = ctx.args.option.lower() |
246 w_std('%s = %s' % (option, opt_formater.get(option, |
152 w_std('%s = %s' % (option, opt_formater.get(option, |
247 noop)(ctx.cget(option)))) |
153 noop)(ctx.cget(option)))) |
248 |
154 |
249 |
155 |
250 def config_set(ctx): |
156 def config_set(ctx): |
251 """set a new value for the configuration option""" |
157 """set a new value for the configuration option""" |
252 if ctx.argc < 3: |
158 ctx.hdlr.cfg_set(ctx.args.option.lower(), ctx.args.value) |
253 usage(EX_MISSING_ARGS, _('Missing option and new value.'), ctx.scmd) |
|
254 if ctx.argc < 4: |
|
255 usage(EX_MISSING_ARGS, _('Missing new configuration value.'), |
|
256 ctx.scmd) |
|
257 ctx.hdlr.cfg_set(ctx.args[2].lower(), ctx.args[3]) |
|
258 |
159 |
259 |
160 |
260 def configure(ctx): |
161 def configure(ctx): |
261 """start interactive configuration mode""" |
162 """start interactive configuration mode""" |
262 if ctx.argc < 3: |
163 ctx.hdlr.configure(ctx.args.section) |
263 ctx.hdlr.configure() |
|
264 else: |
|
265 ctx.hdlr.configure(ctx.args[2].lower()) |
|
266 |
164 |
267 |
165 |
268 def domain_add(ctx): |
166 def domain_add(ctx): |
269 """create a new domain""" |
167 """create a new domain""" |
270 if ctx.argc < 3: |
168 fqdn = ctx.args.fqdn.lower() |
271 usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) |
169 transport = ctx.args.transport.lower if ctx.args.transport else None |
272 elif ctx.argc < 4: |
170 ctx.hdlr.domain_add(fqdn, transport) |
273 ctx.hdlr.domain_add(ctx.args[2].lower()) |
|
274 else: |
|
275 ctx.hdlr.domain_add(ctx.args[2].lower(), ctx.args[3]) |
|
276 if ctx.cget('domain.auto_postmaster'): |
171 if ctx.cget('domain.auto_postmaster'): |
277 w_std(_('Creating account for postmaster@%s') % ctx.args[2].lower()) |
172 w_std(_('Creating account for postmaster@%s') % fqdn) |
278 ctx.scmd = 'useradd' |
173 ctx.args.scmd = 'useradd' |
279 ctx.args = [prog, ctx.scmd, 'postmaster@' + ctx.args[2].lower()] |
174 ctx.args.address = 'postmaster@%s' % fqdn |
280 ctx.argc = 3 |
175 ctx.args.password = None |
281 user_add(ctx) |
176 user_add(ctx) |
282 |
177 |
283 |
178 |
284 def domain_delete(ctx): |
179 def domain_delete(ctx): |
285 """delete the given domain and all its alias domains""" |
180 """delete the given domain and all its alias domains""" |
286 if ctx.argc < 3: |
181 ctx.hdlr.domain_delete(ctx.args.fqdn.lower(), ctx.args.force) |
287 usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) |
|
288 elif ctx.argc < 4: |
|
289 ctx.hdlr.domain_delete(ctx.args[2].lower()) |
|
290 elif ctx.args[3].lower() == 'force': |
|
291 ctx.hdlr.domain_delete(ctx.args[2].lower(), True) |
|
292 else: |
|
293 usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[3], |
|
294 ctx.scmd) |
|
295 |
182 |
296 |
183 |
297 def domain_info(ctx): |
184 def domain_info(ctx): |
298 """display information about the given domain""" |
185 """display information about the given domain""" |
299 if ctx.argc < 3: |
186 fqdn = ctx.args.fqdn.lower() |
300 usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) |
187 details = ctx.args.details |
301 if ctx.argc < 4: |
|
302 details = None |
|
303 else: |
|
304 details = ctx.args[3].lower() |
|
305 if details not in ('accounts', 'aliasdomains', 'aliases', 'full', |
|
306 'relocated', 'catchall'): |
|
307 usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % details, |
|
308 ctx.scmd) |
|
309 try: |
188 try: |
310 info = ctx.hdlr.domain_info(ctx.args[2].lower(), details) |
189 info = ctx.hdlr.domain_info(fqdn, details) |
311 except VMMError as err: |
190 except VMMError as err: |
312 if err.code is DOMAIN_ALIAS_EXISTS: |
191 if err.code is DOMAIN_ALIAS_EXISTS: |
313 w_err(0, ctx.plan_a_b % {'subcommand': 'aliasdomaininfo', |
192 w_err(0, ctx.plan_a_b % {'subcommand': 'aliasdomaininfo', |
314 'object': ctx.args[2].lower()}) |
193 'object': fqdn}) |
315 ctx.scmd = ctx.args[1] = 'aliasdomaininfo' |
194 ctx.args.scmd = 'aliasdomaininfo' |
316 aliasdomain_info(ctx) |
195 aliasdomain_info(ctx) |
317 else: |
196 else: |
318 raise |
197 raise |
319 else: |
198 else: |
320 q_limit = 'Storage: %(bytes)s; Messages: %(messages)s' |
199 q_limit = 'Storage: %(bytes)s; Messages: %(messages)s' |
654 _print_list(info[1], _('alias addresses')) |
390 _print_list(info[1], _('alias addresses')) |
655 |
391 |
656 |
392 |
657 def user_name(ctx): |
393 def user_name(ctx): |
658 """set or update the real name for an address""" |
394 """set or update the real name for an address""" |
659 if ctx.argc < 3: |
395 ctx.hdlr.user_name(ctx.args.address.lower(), ctx.args.name) |
660 usage(EX_MISSING_ARGS, _("Missing e-mail address and user's name."), |
|
661 ctx.scmd) |
|
662 elif ctx.argc < 4: |
|
663 name = None |
|
664 else: |
|
665 name = ctx.args[3] |
|
666 ctx.hdlr.user_name(ctx.args[2].lower(), name) |
|
667 |
396 |
668 |
397 |
669 def user_password(ctx): |
398 def user_password(ctx): |
670 """update the password for the given address""" |
399 """update the password for the given address""" |
671 if ctx.argc < 3: |
400 ctx.hdlr.user_password(ctx.args.address.lower(), ctx.args.password) |
672 usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) |
|
673 elif ctx.argc < 4: |
|
674 password = None |
|
675 else: |
|
676 password = ctx.args[3] |
|
677 ctx.hdlr.user_password(ctx.args[2].lower(), password) |
|
678 |
401 |
679 |
402 |
680 def user_note(ctx): |
403 def user_note(ctx): |
681 """update the note of the given address""" |
404 """update the note of the given address""" |
682 if ctx.argc < 3: |
405 ctx.hdlr.user_note(ctx.args.address.lower(), ctx.args.note) |
683 usage(EX_MISSING_ARGS, _('Missing e-mail address.'), |
|
684 ctx.scmd) |
|
685 elif ctx.argc < 4: |
|
686 note = None |
|
687 else: |
|
688 note = ' '.join(ctx.args[3:]) |
|
689 ctx.hdlr.user_note(ctx.args[2].lower(), note) |
|
690 |
406 |
691 |
407 |
692 def user_quota(ctx): |
408 def user_quota(ctx): |
693 """update the quota limit for the given address""" |
409 """update the quota limit for the given address""" |
694 if ctx.argc < 3: |
410 ctx.hdlr.user_quotalimit(ctx.args.address.lower(), ctx.args.storage, |
695 usage(EX_MISSING_ARGS, _('Missing e-mail address and storage value.'), |
411 ctx.args.messages) |
696 ctx.scmd) |
|
697 elif ctx.argc < 4: |
|
698 usage(EX_MISSING_ARGS, _('Missing storage value.'), ctx.scmd) |
|
699 if ctx.args[3] != 'domain': |
|
700 try: |
|
701 bytes_ = size_in_bytes(ctx.args[3]) |
|
702 except (ValueError, TypeError): |
|
703 usage(INVALID_ARGUMENT, _("Invalid storage value: '%s'") % |
|
704 ctx.args[3], ctx.scmd) |
|
705 else: |
|
706 bytes_ = ctx.args[3] |
|
707 if ctx.argc < 5: |
|
708 messages = 0 |
|
709 else: |
|
710 try: |
|
711 messages = int(ctx.args[4]) |
|
712 except ValueError: |
|
713 usage(INVALID_ARGUMENT, |
|
714 _("Not a valid number of messages: '%s'") % ctx.args[4], |
|
715 ctx.scmd) |
|
716 ctx.hdlr.user_quotalimit(ctx.args[2].lower(), bytes_, messages) |
|
717 |
412 |
718 |
413 |
719 def user_services(ctx): |
414 def user_services(ctx): |
720 """allow all named service and block the uncredited.""" |
415 """allow all named service and block the uncredited.""" |
721 if ctx.argc < 3: |
416 if 'domain' in ctx.args.services: |
722 usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) |
417 services = ['domain'] |
723 services = [] |
418 else: |
724 if ctx.argc >= 4: |
419 services = ctx.args.services |
725 services.extend([service.lower() for service in ctx.args[3:]]) |
420 ctx.hdlr.user_services(ctx.args.address.lower(), *services) |
726 unknown = [service for service in services if service not in SERVICES] |
|
727 if unknown and ctx.args[3] != 'domain': |
|
728 usage(INVALID_ARGUMENT, _('Invalid service arguments: %s') % |
|
729 ' '.join(unknown), ctx.scmd) |
|
730 ctx.hdlr.user_services(ctx.args[2].lower(), *services) |
|
731 |
421 |
732 |
422 |
733 def user_transport(ctx): |
423 def user_transport(ctx): |
734 """update the transport of the given address""" |
424 """update the transport of the given address""" |
735 if ctx.argc < 3: |
425 ctx.hdlr.user_transport(ctx.args.address.lower(), ctx.args.transport) |
736 usage(EX_MISSING_ARGS, _('Missing e-mail address and transport.'), |
426 |
737 ctx.scmd) |
427 |
738 if ctx.argc < 4: |
428 def setup_parser(): |
739 usage(EX_MISSING_ARGS, _('Missing transport.'), ctx.scmd) |
429 """Create the argument parser, add all the subcommands and return it.""" |
740 ctx.hdlr.user_transport(ctx.args[2].lower(), ctx.args[3]) |
430 class ArgParser(ArgumentParser): |
741 |
431 """This class fixes the 'width detection'.""" |
742 |
432 def _get_formatter(self): |
743 def usage(errno, errmsg, subcommand=None): |
433 return self.formatter_class(prog=self.prog, width=WS_ROWS, |
744 """print usage message for the given command or all commands. |
434 max_help_position=26) |
745 When errno > 0, sys,exit(errno) will interrupt the program. |
435 |
746 """ |
436 class VersionAction(Action): |
747 if subcommand and subcommand in cmd_map: |
437 """Show version and copyright information.""" |
748 w_err(errno, _("Error: %s") % errmsg, |
438 def __call__(self, parser, namespace, values, option_string=None): |
749 _("usage: ") + cmd_map[subcommand].usage) |
439 """implements the Action API.""" |
750 |
440 vers_info = _('{program}, version {version} (from {rel_date})\n' |
751 # TP: Please adjust translated words like the original text. |
441 'Python {py_vers} on {sysname}'.format( |
752 # (It's a table header.) Extract from usage text: |
442 program=parser.prog, version=__version__, |
753 # usage: vmm subcommand arguments |
443 rel_date=strftime( |
754 # short long |
444 locale.nl_langinfo(locale.D_FMT), |
755 # subcommand arguments |
445 strptime(__date__, '%Y-%m-%d')), |
756 # |
446 py_vers=platform.python_version(), |
757 # da domainadd fqdn [transport] |
447 sysname=platform.system())) |
758 # dd domaindelete fqdn [force] |
448 copy_info = _('{copyright}\n{program} is free software and comes ' |
759 u_head = _("""usage: %s subcommand arguments |
449 'with ABSOLUTELY NO WARRANTY.'.format( |
760 short long |
450 copyright=__copyright__, program=parser.prog)) |
761 subcommand arguments\n""") % prog |
451 parser.exit(message='\n\n'.join((vers_info, copy_info)) + '\n') |
762 order = sorted(list(cmd_map.keys())) |
452 |
763 w_err(0, u_head) |
453 def quota_storage(string): |
764 for key in order: |
454 if string == 'domain': |
765 scmd = cmd_map[key] |
455 return string |
766 w_err(0, ' %-5s %-19s %s' % (scmd.alias, scmd.name, scmd.args)) |
456 try: |
767 w_err(errno, '', _("Error: %s") % errmsg) |
457 storage = size_in_bytes(string) |
768 |
458 except (TypeError, ValueError) as error: |
769 |
459 raise ArgumentTypeError(str(error)) |
770 def version(ctx_unused): |
460 return storage |
771 """Write version and copyright information to stdout.""" |
461 |
772 w_std('%s, %s %s (%s %s)\nPython %s %s %s\n\n%s\n%s %s' % (prog, |
462 old_rw = txt_wrpr.replace_whitespace |
773 # TP: The words 'from', 'version' and 'on' are used in |
463 txt_wrpr.replace_whitespace = False |
774 # the version information, e.g.: |
464 fill = lambda t: '\n'.join(txt_wrpr.fill(l) for l in t.splitlines(True)) |
775 # vmm, version 0.5.2 (from 09/09/09) |
465 mklst = lambda iterable: '\n\t - ' + '\n\t - '.join(iterable) |
776 # Python 2.5.4 on FreeBSD |
466 |
777 _('version'), __version__, _('from'), |
467 description = _('%(prog)s - command line tool to manage email ' |
778 strftime(locale.nl_langinfo(locale.D_FMT), |
468 'domains/accounts/aliases/...') |
779 strptime(__date__, '%Y-%m-%d')), |
469 epilog = _('use "%(prog)s <subcommand> -h" for information about the ' |
780 os.sys.version.split()[0], _('on'), os.uname()[0], |
470 'given subcommand') |
781 __copyright__, prog, |
471 parser = ArgParser(description=description, epilog=epilog) |
782 _('is free software and comes with ABSOLUTELY NO WARRANTY.'))) |
472 parser.add_argument('-v', '--version', action=VersionAction, nargs=0, |
783 |
473 help=_("show %(prog)s's version and copyright " |
784 |
474 "information and exit")) |
785 def update_cmd_map(): |
475 subparsers = parser.add_subparsers(metavar=_('<subcommand>'), |
786 """Update the cmd_map, after gettext's _ was installed.""" |
476 title=_('list of available subcommands')) |
787 cmd = Command |
477 a = subparsers.add_parser |
788 cmd_map.update({ |
478 |
789 # Account commands |
479 ### |
790 'getuser': cmd('getuser', 'gu', get_user, 'uid', |
480 # general subcommands |
791 _('get the address of the user with the given UID')), |
481 ### |
792 'useradd': cmd('useradd', 'ua', user_add, 'address [password]', |
482 cg = a('configget', aliases=('cg',), |
793 _('create a new e-mail user with the given address')), |
483 help=_('show the actual value of the configuration option'), |
794 'userdelete': cmd('userdelete', 'ud', user_delete, 'address [force]', |
484 epilog=_("This subcommand is used to display the actual value of " |
795 _('delete the specified user')), |
485 "the given configuration option.")) |
796 'userinfo': cmd('userinfo', 'ui', user_info, 'address [details]', |
486 cg.add_argument('option', help=_('the name of a configuration option')) |
797 _('display information about the given address')), |
487 cg.set_defaults(func=config_get, scmd='configget') |
798 'username': cmd('username', 'un', user_name, 'address [name]', |
488 |
799 _('set, update or delete the real name for an address')), |
489 cs = a('configset', aliases=('cs',), |
800 'userpassword': cmd('userpassword', 'up', user_password, |
490 help=_('set a new value for the configuration option'), |
801 'address [password]', |
491 epilog=fill(_("Use this subcommand to set or update a single " |
802 _('update the password for the given address')), |
492 "configuration option's value. option is the configuration " |
803 'userquota': cmd('userquota', 'uq', user_quota, |
493 "option, value is the option's new value.\n\nNote: This " |
804 'address storage [messages] | address domain', |
494 "subcommand will create a new vmm.cfg without any comments. " |
805 _('update the quota limit for the given address')), |
495 "Your current configuration file will be backed as " |
806 'userservices': cmd('userservices', 'us', user_services, |
496 "vmm.cfg.bak.")), |
807 'address [service ...] | address domain', |
497 formatter_class=RawDescriptionHelpFormatter) |
808 _('enables the specified services and disables all ' |
498 cs.add_argument('option', help=_('the name of a configuration option')) |
809 'not specified services')), |
499 cs.add_argument('value', help=_("the option's new value")) |
810 'usertransport': cmd('usertransport', 'ut', user_transport, |
500 cs.set_defaults(func=config_set, scmd='configset') |
811 'address transport | address domain', |
501 |
812 _('update the transport of the given address')), |
502 sections = ('account', 'bin', 'database', 'domain', 'mailbox', 'misc') |
813 'usernote': cmd('usernote', 'uo', user_note, 'address [note]', |
503 cf = a('configure', aliases=('cf',), |
814 _('set, update or delete the note of the given address')), |
504 help=_('start interactive configuration mode'), |
815 # Alias commands |
505 epilog=fill(_("Starts the interactive configuration for all " |
816 'aliasadd': cmd('aliasadd', 'aa', alias_add, 'address destination ...', |
506 "configuration sections.\n\nIn this process the currently set " |
817 _('create a new alias e-mail address with one or more ' |
507 "value of each option will be displayed in square brackets. " |
818 'destinations')), |
508 "If no value is configured, the default value of each option " |
819 'aliasdelete': cmd('aliasdelete', 'ad', alias_delete, |
509 "will be displayed in square brackets. Press the return key, " |
820 'address [destination ...]', |
510 "to accept the displayed value.\n\n" |
821 _('delete the specified alias e-mail address or one ' |
511 "If the optional argument section is given, only the " |
822 'of its destinations')), |
512 "configuration options from the given section will be " |
823 'aliasinfo': cmd('aliasinfo', 'ai', alias_info, 'address', |
513 "displayed and will be configurable. The following sections " |
824 _('show the destination(s) of the specified alias')), |
514 "are available:\n") + mklst(sections)), |
825 # AliasDomain commands |
515 formatter_class=RawDescriptionHelpFormatter) |
826 'aliasdomainadd': cmd('aliasdomainadd', 'ada', aliasdomain_add, |
516 cf.add_argument('-s', choices=sections, metavar='SECTION', dest='section', |
827 'fqdn destination', |
517 help=_("configure only options of the given section")) |
828 _('create a new alias for an existing domain')), |
518 cf.set_defaults(func=configure, scmd='configure') |
829 'aliasdomaindelete': cmd('aliasdomaindelete', 'add', aliasdomain_delete, |
519 |
830 'fqdn', _('delete the specified alias domain')), |
520 gu = a('getuser', aliases=('gu',), |
831 'aliasdomaininfo': cmd('aliasdomaininfo', 'adi', aliasdomain_info, 'fqdn', |
521 help=_('get the address of the user with the given UID'), |
832 _('show the destination of the given alias domain')), |
522 epilog=_("If only the uid is available, for example from process " |
833 'aliasdomainswitch': cmd('aliasdomainswitch', 'ads', aliasdomain_switch, |
523 "list, the subcommand getuser will show the user's " |
834 'fqdn destination', _('assign the given alias ' |
524 "address.")) |
835 'domain to an other domain')), |
525 gu.add_argument('uid', type=int, help=_("a user's unique identifier")) |
836 # CatchallAlias commands |
526 gu.set_defaults(func=get_user, scmd='getuser') |
837 'catchalladd': cmd('catchalladd', 'caa', catchall_add, |
527 |
838 'fqdn destination ...', |
528 ll = a('listaddresses', aliases=('ll',), |
839 _('add one or more catch-all destinations for a ' |
529 help=_('list all addresses or search for addresses by pattern'), |
840 'domain')), |
530 epilog=fill(_("This command lists all defined addresses. " |
841 'catchalldelete': cmd('catchalldelete', 'cad', catchall_delete, |
531 "Addresses belonging to alias-domains are prefixed with a '-', " |
842 'fqdn [destination ...]', |
532 "addresses of regular domains with a '+'. Additionally, the " |
843 _('delete the specified catch-all destination or all ' |
533 "letters 'u', 'a', and 'r' indicate the type of each address: " |
844 'of a domain\'s destinations')), |
534 "user, alias and relocated respectively. The output can be " |
845 'catchallinfo': cmd('catchallinfo', 'cai', catchall_info, 'fqdn', |
535 "limited with an optional pattern.\n\nTo perform a wild card " |
846 _('show the catch-all destination(s) of the ' |
536 "search, the % character can be used at the start and/or the " |
847 'specified domain')), |
537 "end of the pattern.")), |
848 # Domain commands |
538 formatter_class=RawDescriptionHelpFormatter) |
849 'domainadd': cmd('domainadd', 'da', domain_add, 'fqdn [transport]', |
539 ll.add_argument('-p', help=_("the pattern to search for"), |
850 _('create a new domain')), |
540 metavar='PATTERN', dest='pattern') |
851 'domaindelete': cmd('domaindelete', 'dd', domain_delete, 'fqdn [force]', |
541 ll.set_defaults(func=list_addresses, scmd='listaddresses') |
852 _('delete the given domain and all its alias domains')), |
542 |
853 'domaininfo': cmd('domaininfo', 'di', domain_info, 'fqdn [details]', |
543 la = a('listaliases', aliases=('la',), |
854 _('display information about the given domain')), |
544 help=_('list all aliases or search for aliases by pattern'), |
855 'domainquota': cmd('domainquota', 'dq', domain_quota, |
545 epilog=fill(_("This command lists all defined aliases. Aliases " |
856 'fqdn storage [messages] [force]', |
546 "belonging to alias-domains are prefixed with a '-', addresses " |
857 _('update the quota limit of the specified domain')), |
547 "of regular domains with a '+'. The output can be limited " |
858 'domainservices': cmd('domainservices', 'ds', domain_services, |
548 "with an optional pattern.\n\nTo perform a wild card search, " |
859 'fqdn [service ...] [force]', |
549 "the % character can be used at the start and/or the end of " |
860 _('enables the specified services and disables all ' |
550 "the pattern.")), |
861 'not specified services of the given domain')), |
551 formatter_class=RawDescriptionHelpFormatter) |
862 'domaintransport': cmd('domaintransport', 'dt', domain_transport, |
552 la.add_argument('-p', help=_("the pattern to search for"), |
863 'fqdn transport [force]', |
553 metavar='PATTERN', dest='pattern') |
864 _('update the transport of the specified domain')), |
554 la.set_defaults(func=list_aliases, scmd='listaliases') |
865 'domainnote': cmd('domainnote', 'do', domain_note, 'fqdn [note]', |
555 |
866 _('set, update or delete the note of the given domain')), |
556 ld = a('listdomains', aliases=('ld',), |
867 # List commands |
557 help=_('list all domains or search for domains by pattern'), |
868 'listdomains': cmd('listdomains', 'ld', list_domains, '[pattern]', |
558 epilog=fill(_("This subcommand lists all available domains. All " |
869 _('list all domains or search for domains by pattern')), |
559 "domain names will be prefixed either with `[+]', if the " |
870 'listaddresses': cmd('listaddresses', 'll', list_addresses, '[pattern]', |
560 "domain is a primary domain, or with `[-]', if it is an alias " |
871 _('list all addresses or search for addresses by ' |
561 "domain name. The output can be limited with an optional " |
872 'pattern')), |
562 "pattern.\n\nTo perform a wild card search, the % character " |
873 'listusers': cmd('listusers', 'lu', list_users, '[pattern]', |
563 "can be used at the start and/or the end of the pattern.")), |
874 _('list all user accounts or search for accounts by ' |
564 formatter_class=RawDescriptionHelpFormatter) |
875 'pattern')), |
565 ld.add_argument('-p', help=_("the pattern to search for"), |
876 'listaliases': cmd('listaliases', 'la', list_aliases, '[pattern]', |
566 metavar='PATTERN', dest='pattern') |
877 _('list all aliases or search for aliases by pattern')), |
567 ld.set_defaults(func=list_domains, scmd='listdomains') |
878 'listrelocated': cmd('listrelocated', 'lr', list_relocated, '[pattern]', |
568 |
879 _('list all relocated users or search for relocated ' |
569 lr = a('listrelocated', aliases=('lr',), |
880 'users by pattern')), |
570 help=_('list all relocated users or search for relocated users by ' |
881 # Relocated commands |
571 'pattern'), |
882 'relocatedadd': cmd('relocatedadd', 'ra', relocated_add, |
572 epilog=fill(_("This command lists all defined relocated addresses. " |
883 'address newaddress', |
573 "Relocated entries belonging to alias-domains are prefixed " |
884 _('create a new record for a relocated user')), |
574 "with a '-', addresses of regular domains with a '+'. The " |
885 'relocateddelete': cmd('relocateddelete', 'rd', relocated_delete, |
575 "output can be limited with an optional pattern.\n\nTo " |
886 'address', |
576 "perform a wild card search, the % character can be used at " |
887 _('delete the record of the relocated user')), |
577 "the start and/or the end of the pattern.")), |
888 'relocatedinfo': cmd('relocatedinfo', 'ri', relocated_info, 'address', |
578 formatter_class=RawDescriptionHelpFormatter) |
889 _('print information about a relocated user')), |
579 lr.add_argument('-p', help=_("the pattern to search for"), |
890 # cli commands |
580 metavar='PATTERN', dest='pattern') |
891 'configget': cmd('configget', 'cg', config_get, 'option', |
581 lr.set_defaults(func=list_relocated, scmd='listrelocated') |
892 _('show the actual value of the configuration option')), |
582 |
893 'configset': cmd('configset', 'cs', config_set, 'option value', |
583 lu = a('listusers', aliases=('lu',), |
894 _('set a new value for the configuration option')), |
584 help=_('list all user accounts or search for accounts by pattern'), |
895 'configure': cmd('configure', 'cf', configure, '[section]', |
585 epilog=fill(_("This command lists all user accounts. User accounts " |
896 _('start interactive configuration mode')), |
586 "belonging to alias-domains are prefixed with a '-', " |
897 'listpwschemes': cmd('listpwschemes', 'lp', list_pwschemes, '', |
587 "addresses of regular domains with a '+'. The output can be " |
898 _('lists all usable password schemes and password ' |
588 "limited with an optional pattern.\n\nTo perform a wild card " |
899 'encoding suffixes')), |
589 "search, the % character can be used at the start and/or the " |
900 'help': cmd('help', 'h', help_, '[subcommand]', |
590 "end of the pattern.")), |
901 _('show a help overview or help for the given subcommand')), |
591 formatter_class=RawDescriptionHelpFormatter) |
902 'version': cmd('version', 'v', version, '', |
592 lu.add_argument('-p', help=_("the pattern to search for"), |
903 _('show version and copyright information')), |
593 metavar='PATTERN', dest='pattern') |
904 }) |
594 lu.set_defaults(func=list_users, scmd='listusers') |
|
595 |
|
596 lp = a('listpwschemes', aliases=('lp',), |
|
597 help=_('lists all usable password schemes and password encoding ' |
|
598 'suffixes'), |
|
599 epilog=fill(_("This subcommand lists all password schemes which " |
|
600 "could be used in the vmm.cfg as value of the " |
|
601 "misc.password_scheme option. The output varies, depending " |
|
602 "on the used Dovecot version and the system's libc.\nWhen " |
|
603 "your Dovecot installation isn't too old, you will see " |
|
604 "additionally a few usable encoding suffixes. One of them can " |
|
605 "be appended to the password scheme.")), |
|
606 formatter_class=RawDescriptionHelpFormatter) |
|
607 lp.set_defaults(func=list_pwschemes, scmd='listpwschemes') |
|
608 |
|
609 ### |
|
610 # domain subcommands |
|
611 ### |
|
612 da = a('domainadd', aliases=('da',), help=_('create a new domain'), |
|
613 epilog=fill(_("Adds the new domain into the database and creates " |
|
614 "the domain directory.\n\nIf the optional argument transport " |
|
615 "is given, it will override the default transport " |
|
616 "(domain.transport) from vmm.cfg. The specified transport " |
|
617 "will be the default transport for all new accounts in this " |
|
618 "domain.")), |
|
619 formatter_class=RawDescriptionHelpFormatter) |
|
620 da.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
621 da.add_argument('-t', metavar='TRANSPORT', dest='transport', |
|
622 help=_('a Postfix transport (transport: or ' |
|
623 'transport:nexthop)')) |
|
624 da.set_defaults(func=domain_add, scmd='domainadd') |
|
625 |
|
626 details = ('accounts', 'aliasdomains', 'aliases', 'catchall', 'relocated', |
|
627 'full') |
|
628 di = a('domaininfo', aliases=('di',), |
|
629 help=_('display information about the given domain'), |
|
630 epilog=fill(_("This subcommand shows some information about the " |
|
631 "given domain.\n\nFor a more detailed information about the " |
|
632 "domain the optional argument details can be specified. A " |
|
633 "possible details value can be one of the following six " |
|
634 "keywords:\n") + mklst(details)), |
|
635 formatter_class=RawDescriptionHelpFormatter) |
|
636 di.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
637 di.add_argument('-d', choices=details, dest='details', metavar='DETAILS', |
|
638 help=_('additionally details to display')) |
|
639 di.set_defaults(func=domain_info, scmd='domaininfo') |
|
640 |
|
641 do = a('domainnote', aliases=('do',), |
|
642 help=_('set, update or delete the note of the given domain'), |
|
643 epilog=_('With this subcommand, it is possible to attach a note to ' |
|
644 'the specified domain. Without an argument, an existing ' |
|
645 'note is removed.')) |
|
646 do.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
647 do.add_argument('-n', metavar='NOTE', dest='note', |
|
648 help=_('the note that should be set')) |
|
649 do.set_defaults(func=domain_note, scmd='domainnote') |
|
650 |
|
651 dq = a('domainquota', aliases=('dq',), |
|
652 help=_('update the quota limit of the specified domain'), |
|
653 epilog=fill(_("This subcommand is used to configure a new quota " |
|
654 "limit for the accounts of the domain - not for the domain " |
|
655 "itself.\n\nThe default quota limit for accounts is defined " |
|
656 "in the vmm.cfg (domain.quota_bytes and " |
|
657 "domain.quota_messages).\n\nThe new quota limit will affect " |
|
658 "only those accounts for which the limit has not been " |
|
659 "overridden. If you want to restore the default to all " |
|
660 "accounts, you may pass the optional argument --force. When " |
|
661 "the argument messages was omitted the default number of " |
|
662 "messages 0 (zero) will be applied.")), |
|
663 formatter_class=RawDescriptionHelpFormatter) |
|
664 dq.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
665 dq.add_argument('storage', type=quota_storage, |
|
666 help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G ' |
|
667 'or 2048M',)) |
|
668 dq.add_argument('-m', default=0, type=int, metavar='MESSAGES', |
|
669 dest='messages', |
|
670 help=_('quota limit in number of messages (default: 0)')) |
|
671 dq.add_argument('--force', action='store_true', |
|
672 help=_('enforce the limit for all accounts')) |
|
673 dq.set_defaults(func=domain_quota, scmd='domainquota') |
|
674 |
|
675 ds = a('domainservices', aliases=('ds',), |
|
676 help=_('enables the specified services and disables all not ' |
|
677 'specified services of the given domain'), |
|
678 epilog=fill(_("To define which services could be used by the users " |
|
679 "of the domain — with the given fqdn — use this " |
|
680 "subcommand.\n\nEach specified service will be enabled/" |
|
681 "usable. All other services will be deactivated/unusable. " |
|
682 "Possible service names are: imap, pop3, sieve and smtp.\nThe " |
|
683 "new service set will affect only those accounts for which " |
|
684 "the set has not been overridden. If you want to restore the " |
|
685 "default to all accounts, you may pass --force.")), |
|
686 formatter_class=RawDescriptionHelpFormatter) |
|
687 ds.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
688 ds.add_argument('-s', choices=SERVICES, |
|
689 help=_('services which should be usable'), |
|
690 metavar='SERVICE', nargs='+', dest='services') |
|
691 ds.add_argument('--force', action='store_true', |
|
692 help=_('enforce the service set for all accounts')) |
|
693 ds.set_defaults(func=domain_services, scmd='domainservices') |
|
694 |
|
695 dt = a('domaintransport', aliases=('dt',), |
|
696 help=_('update the transport of the specified domain'), |
|
697 epilog=fill(_("A new transport for the indicated domain can be set " |
|
698 "with this subcommand.\n\nThe new transport will affect only " |
|
699 "those accounts for which the transport has not been " |
|
700 "overridden. If you want to restore the default to all " |
|
701 "accounts, you may pass --force.")), |
|
702 formatter_class=RawDescriptionHelpFormatter) |
|
703 dt.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
704 dt.add_argument('transport', help=_('a Postfix transport (transport: or ' |
|
705 'transport:nexthop)')) |
|
706 dt.add_argument('--force', action='store_true', |
|
707 help=_('enforce the transport for all accounts')) |
|
708 dt.set_defaults(func=domain_transport, scmd='domaintransport') |
|
709 |
|
710 dd = a('domaindelete', aliases=('dd',), |
|
711 help=_('delete the given domain and all its alias domains'), |
|
712 epilog=fill(_("This subcommand deletes the domain specified by " |
|
713 "fqdn.\n\nIf there are accounts, aliases and/or relocated " |
|
714 "users assigned to the given domain, vmm will abort the " |
|
715 "requested operation and show an error message. If you know, " |
|
716 "what you are doing, you can specify the optional argument " |
|
717 "--force.\n\nIf you really always know what you are doing, " |
|
718 "edit your vmm.cfg and set the option domain.force_deletion " |
|
719 "to true.")), |
|
720 formatter_class=RawDescriptionHelpFormatter) |
|
721 dd.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
722 dd.add_argument('--force', action='store_true', |
|
723 help=_('also delete all accounts, aliases and/or ' |
|
724 'relocated users')) |
|
725 dd.set_defaults(func=domain_delete, scmd='domaindelete') |
|
726 |
|
727 ### |
|
728 # alias domain subcommands |
|
729 ### |
|
730 ada = a('aliasdomainadd', aliases=('ada',), |
|
731 help=_('create a new alias for an existing domain'), |
|
732 epilog=_('This subcommand adds the new alias domain (fqdn) to ' |
|
733 'the destination domain that should be aliased.')) |
|
734 ada.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
735 ada.add_argument('destination', |
|
736 help=_('the fqdn of the destination domain')) |
|
737 ada.set_defaults(func=aliasdomain_add, scmd='aliasdomainadd') |
|
738 |
|
739 adi = a('aliasdomaininfo', aliases=('adi',), |
|
740 help=_('show the destination of the given alias domain'), |
|
741 epilog=_('This subcommand shows to which domain the alias domain ' |
|
742 'fqdn is assigned to.')) |
|
743 adi.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
744 adi.set_defaults(func=aliasdomain_info, scmd='aliasdomaininfo') |
|
745 |
|
746 ads = a('aliasdomainswitch', aliases=('ads',), |
|
747 help=_('assign the given alias domain to an other domain'), |
|
748 epilog=_('If the destination of the existing alias domain fqdn ' |
|
749 'should be switched to another destination use this ' |
|
750 'subcommand.')) |
|
751 ads.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
752 ads.add_argument('destination', |
|
753 help=_('the fqdn of the destination domain')) |
|
754 ads.set_defaults(func=aliasdomain_switch, scmd='aliasdomainswitch') |
|
755 |
|
756 add = a('aliasdomaindelete', aliases=('add',), |
|
757 help=_('delete the specified alias domain'), |
|
758 epilog=_('Use this subcommand if the alias domain fqdn should be ' |
|
759 'removed.')) |
|
760 add.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
761 add.set_defaults(func=aliasdomain_delete, scmd='aliasdomaindelete') |
|
762 |
|
763 ### |
|
764 # account subcommands |
|
765 ### |
|
766 ua = a('useradd', aliases=('ua',), |
|
767 help=_('create a new e-mail user with the given address'), |
|
768 epilog=fill(_('Use this subcommand to create a new e-mail account ' |
|
769 'for the given address.\n\nIf the password is not provided, ' |
|
770 'vmm will prompt for it interactively. When no password is ' |
|
771 'provided and account.random_password is set to true, vmm ' |
|
772 'will generate a random password and print it to stdout ' |
|
773 'after the account has been created.')), |
|
774 formatter_class=RawDescriptionHelpFormatter) |
|
775 ua.add_argument('address', |
|
776 help=_("an account's e-mail address (local-part@fqdn)")) |
|
777 ua.add_argument('-p', metavar='PASSWORD', dest='password', |
|
778 help=_("the new user's password")) |
|
779 ua.set_defaults(func=user_add, scmd='useradd') |
|
780 |
|
781 details = ('aliases', 'du', 'full') |
|
782 ui = a('userinfo', aliases=('ui',), |
|
783 help=_('display information about the given address'), |
|
784 epilog=fill(_('This subcommand displays some information about ' |
|
785 'the account specified by the given address.\n\nIf the ' |
|
786 'optional argument details is given some more information ' |
|
787 'will be displayed.\nPossible values for details are:\n') + |
|
788 mklst(details)), |
|
789 formatter_class=RawDescriptionHelpFormatter) |
|
790 ui.add_argument('address', |
|
791 help=_("an account's e-mail address (local-part@fqdn)")) |
|
792 ui.add_argument('-d', choices=details, metavar='DETAILS', dest='details', |
|
793 help=_('additionally details to display')) |
|
794 ui.set_defaults(func=user_info, scmd='userinfo') |
|
795 |
|
796 un = a('username', aliases=('un',), |
|
797 help=_('set, update or delete the real name for an address'), |
|
798 epilog=fill(_("The user's real name can be set/updated with this " |
|
799 "subcommand.\n\nIf no name is given, the value stored for the " |
|
800 "account is erased.")), |
|
801 formatter_class=RawDescriptionHelpFormatter) |
|
802 un.add_argument('address', |
|
803 help=_("an account's e-mail address (local-part@fqdn)")) |
|
804 un.add_argument('-n', help=_("a user's real name"), metavar='NAME', |
|
805 dest='name') |
|
806 un.set_defaults(func=user_name, scmd='username') |
|
807 |
|
808 uo = a('usernote', aliases=('uo',), |
|
809 help=_('set, update or delete the note of the given address'), |
|
810 epilog=_('With this subcommand, it is possible to attach a note to ' |
|
811 'the specified account. Without the note argument, an ' |
|
812 'existing note is removed.')) |
|
813 uo.add_argument('address', |
|
814 help=_("an account's e-mail address (local-part@fqdn)")) |
|
815 uo.add_argument('-n', metavar='NOTE', dest='note', |
|
816 help=_('the note that should be set')) |
|
817 uo.set_defaults(func=user_note, scmd='usernote') |
|
818 |
|
819 up = a('userpassword', aliases=('up',), |
|
820 help=_('update the password for the given address'), |
|
821 epilog=fill(_("The password of an account can be updated with this " |
|
822 "subcommand.\n\nIf no password was provided, vmm will prompt " |
|
823 "for it interactively.")), |
|
824 formatter_class=RawDescriptionHelpFormatter) |
|
825 up.add_argument('address', |
|
826 help=_("an account's e-mail address (local-part@fqdn)")) |
|
827 up.add_argument('-p', metavar='PASSWORD', dest='password', |
|
828 help=_("the user's new password")) |
|
829 up.set_defaults(func=user_password, scmd='userpassword') |
|
830 |
|
831 uq = a('userquota', aliases=('uq',), |
|
832 help=_('update the quota limit for the given address'), |
|
833 epilog=fill(_("This subcommand is used to set a new quota limit " |
|
834 "for the given account.\n\nWhen the argument messages was " |
|
835 "omitted the default number of messages 0 (zero) will be " |
|
836 "applied.\n\nInstead of a storage limit pass the keyword " |
|
837 "'domain' to remove the account-specific override, causing " |
|
838 "the domain's value to be in effect.")), |
|
839 formatter_class=RawDescriptionHelpFormatter) |
|
840 uq.add_argument('address', |
|
841 help=_("an account's e-mail address (local-part@fqdn)")) |
|
842 uq.add_argument('storage', type=quota_storage, |
|
843 help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G ' |
|
844 'or 2048M')) |
|
845 uq.add_argument('-m', default=0, type=int, metavar='MESSAGES', |
|
846 dest='messages', |
|
847 help=_('quota limit in number of messages (default: 0)')) |
|
848 uq.set_defaults(func=user_quota, scmd='userquota') |
|
849 |
|
850 us = a('userservices', aliases=('us',), |
|
851 help=_('enable the specified services and disables all not ' |
|
852 'specified services'), |
|
853 epilog=fill(_("To grant a user access to the specified service(s), " |
|
854 "use this command.\n\nAll omitted services will be " |
|
855 "deactivated/unusable for the user with the given " |
|
856 "address.\n\nInstead of any service pass the keyword " |
|
857 "'domain' to remove the account-specific override, causing " |
|
858 "the domain's value to be in effect.")), |
|
859 formatter_class=RawDescriptionHelpFormatter) |
|
860 us.add_argument('address', |
|
861 help=_("an account's e-mail address (local-part@fqdn)")) |
|
862 us.add_argument('-s', choices=SERVICES + ('domain',), |
|
863 help=_('services which should be usable'), |
|
864 metavar='SERVICE', nargs='+', dest='services') |
|
865 us.set_defaults(func=user_services, scmd='userservices') |
|
866 |
|
867 ut = a('usertransport', aliases=('ut',), |
|
868 help=_('update the transport of the given address'), |
|
869 epilog=fill(_("A different transport for an account can be " |
|
870 "specified with this subcommand.\n\nInstead of a transport " |
|
871 "pass the keyword 'domain' to remove the account-specific " |
|
872 "override, causing the domain's value to be in effect.")), |
|
873 formatter_class=RawDescriptionHelpFormatter) |
|
874 ut.add_argument('address', |
|
875 help=_("an account's e-mail address (local-part@fqdn)")) |
|
876 ut.add_argument('transport', help=_('a Postfix transport (transport: or ' |
|
877 'transport:nexthop)')) |
|
878 ut.set_defaults(func=user_transport, scmd='usertransport') |
|
879 |
|
880 ud = a('userdelete', aliases=('ud',), |
|
881 help=_('delete the specified user'), |
|
882 epilog=fill(_('Use this subcommand to delete the account with the ' |
|
883 'given address.\n\nIf there are one or more aliases with an ' |
|
884 'identical destination address, vmm will abort the requested ' |
|
885 'operation and show an error message. To prevent this, ' |
|
886 'give the optional argument --force.')), |
|
887 formatter_class=RawDescriptionHelpFormatter) |
|
888 ud.add_argument('address', |
|
889 help=_("an account's e-mail address (local-part@fqdn)")) |
|
890 ud.add_argument('--force', action='store_true', |
|
891 help=_('also delete assigned alias addresses')) |
|
892 ud.set_defaults(func=user_delete, scmd='userdelete') |
|
893 |
|
894 ### |
|
895 # alias subcommands |
|
896 ### |
|
897 aa = a('aliasadd', aliases=('aa',), |
|
898 help=_('create a new alias e-mail address with one or more ' |
|
899 'destinations'), |
|
900 epilog=fill(_("This subcommand is used to create a new alias " |
|
901 "address with one or more destination addresses.\n\nWithin " |
|
902 "the destination address, the placeholders %n, %d, and %= " |
|
903 "will be replaced by the local part, the domain, or the " |
|
904 "email address with '@' replaced by '=' respectively. In " |
|
905 "combination with alias domains, this enables " |
|
906 "domain-specific destinations.")), |
|
907 formatter_class=RawDescriptionHelpFormatter) |
|
908 aa.add_argument('address', |
|
909 help=_("an alias' e-mail address (local-part@fqdn)")) |
|
910 aa.add_argument('destination', nargs='+', |
|
911 help=_("a destination's e-mail address (local-part@fqdn)")) |
|
912 aa.set_defaults(func=alias_add, scmd='aliasadd') |
|
913 |
|
914 ai = a('aliasinfo', aliases=('ai',), |
|
915 help=_('show the destination(s) of the specified alias'), |
|
916 epilog=_('Information about the alias with the given address can ' |
|
917 'be displayed with this subcommand.')) |
|
918 ai.add_argument('address', |
|
919 help=_("an alias' e-mail address (local-part@fqdn)")) |
|
920 ai.set_defaults(func=alias_info, scmd='aliasinfo') |
|
921 |
|
922 ad = a('aliasdelete', aliases=('ad',), |
|
923 help=_('delete the specified alias e-mail address or one of its ' |
|
924 'destinations'), |
|
925 epilog=fill(_("This subcommand is used to delete one or multiple " |
|
926 "destinations from the alias with the given address.\n\nWhen " |
|
927 "no destination address was specified the alias with all its " |
|
928 "destinations will be deleted.")), |
|
929 formatter_class=RawDescriptionHelpFormatter) |
|
930 ad.add_argument('address', |
|
931 help=_("an alias' e-mail address (local-part@fqdn)")) |
|
932 ad.add_argument('destination', nargs='*', |
|
933 help=_("a destination's e-mail address (local-part@fqdn)")) |
|
934 ad.set_defaults(func=alias_delete, scmd='aliasdelete') |
|
935 |
|
936 ### |
|
937 # catch-all subcommands |
|
938 ### |
|
939 caa = a('catchalladd', aliases=('caa',), |
|
940 help=_('add one or more catch-all destinations for a domain'), |
|
941 epilog=fill(_('This subcommand allows to specify destination ' |
|
942 'addresses for a domain, which shall receive mail addressed ' |
|
943 'to unknown local parts within that domain. Those catch-all ' |
|
944 'aliases hence "catch all" mail to any address in the domain ' |
|
945 '(unless a more specific alias, mailbox or relocated entry ' |
|
946 'exists).\n\nWARNING: Catch-all addresses can cause mail ' |
|
947 'server flooding because spammers like to deliver mail to ' |
|
948 'all possible combinations of names, e.g. to all addresses ' |
|
949 'between abba@example.org and zztop@example.org.')), |
|
950 formatter_class=RawDescriptionHelpFormatter) |
|
951 caa.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
952 caa.add_argument('destination', nargs='+', |
|
953 help=_("a destination's e-mail address (local-part@fqdn)")) |
|
954 caa.set_defaults(func=catchall_add, scmd='catchalladd') |
|
955 |
|
956 cai = a('catchallinfo', aliases=('cai',), |
|
957 help=_('show the catch-all destination(s) of the specified ' |
|
958 'domain'), |
|
959 epilog=_('This subcommand displays information about catch-all ' |
|
960 'aliases defined for a domain.')) |
|
961 cai.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
962 cai.set_defaults(func=catchall_info, scmd='catchallinfo') |
|
963 |
|
964 cad = a('catchalldelete', aliases=('cad',), |
|
965 help=_("delete the specified catch-all destination or all of a " |
|
966 "domain's destinations"), |
|
967 epilog=_('With this subcommand, catch-all aliases defined for a ' |
|
968 'domain can be removed, either all of them, or those ' |
|
969 'destinations which were specified explicitly.')) |
|
970 cad.add_argument('fqdn', help=_('a fully qualified domain name')) |
|
971 cad.add_argument('destination', nargs='*', |
|
972 help=_("a destination's e-mail address (local-part@fqdn)")) |
|
973 cad.set_defaults(func=catchall_delete, scmd='catchalldelete') |
|
974 |
|
975 ### |
|
976 # relocated subcommands |
|
977 ### |
|
978 ra = a('relocatedadd', aliases=('ra',), |
|
979 help=_('create a new record for a relocated user'), |
|
980 epilog=_("A new relocated user can be created with this " |
|
981 "subcommand.")) |
|
982 ra.add_argument('address', help=_("a relocated user's e-mail address " |
|
983 "(local-part@fqdn)")) |
|
984 ra.add_argument('newaddress', |
|
985 help=_('e-mail address where the user can be reached now')) |
|
986 ra.set_defaults(func=relocated_add, scmd='relocatedadd') |
|
987 |
|
988 ri = a('relocatedinfo', aliases=('ri',), |
|
989 help=_('print information about a relocated user'), |
|
990 epilog=_('This subcommand shows the new address of the relocated ' |
|
991 'user with the given address.')) |
|
992 ri.add_argument('address', help=_("a relocated user's e-mail address " |
|
993 "(local-part@fqdn)")) |
|
994 ri.set_defaults(func=relocated_info, scmd='relocatedinfo') |
|
995 |
|
996 rd = a('relocateddelete', aliases=('rd',), |
|
997 help=_('delete the record of the relocated user'), |
|
998 epilog=_('Use this subcommand in order to delete the relocated ' |
|
999 'user with the given address.')) |
|
1000 rd.add_argument('address', help=_("a relocated user's e-mail address " |
|
1001 "(local-part@fqdn)")) |
|
1002 rd.set_defaults(func=relocated_delete, scmd='relocateddelete') |
|
1003 |
|
1004 txt_wrpr.replace_whitespace = old_rw |
|
1005 return parser |
905 |
1006 |
906 |
1007 |
907 def _get_order(ctx): |
1008 def _get_order(ctx): |
908 """returns a tuple with (key, 1||0) tuples. Used by functions, which |
1009 """returns a tuple with (key, 1||0) tuples. Used by functions, which |
909 get a dict from the handler.""" |
1010 get a dict from the handler.""" |
910 order = () |
1011 order = () |
911 if ctx.scmd == 'domaininfo': |
1012 if ctx.args.scmd == 'domaininfo': |
912 order = (('domain name', 0), ('gid', 1), ('domain directory', 0), |
1013 order = (('domain name', 0), ('gid', 1), ('domain directory', 0), |
913 ('quota limit/user', 0), ('active services', 0), |
1014 ('quota limit/user', 0), ('active services', 0), |
914 ('transport', 0), ('alias domains', 0), ('accounts', 0), |
1015 ('transport', 0), ('alias domains', 0), ('accounts', 0), |
915 ('aliases', 0), ('relocated', 0), ('catch-all dests', 0)) |
1016 ('aliases', 0), ('relocated', 0), ('catch-all dests', 0)) |
916 elif ctx.scmd == 'userinfo': |
1017 elif ctx.args.scmd == 'userinfo': |
917 if ctx.argc == 4 and ctx.args[3] != 'aliases' or \ |
1018 if ctx.args.details in ('du', 'full') or \ |
918 ctx.cget('account.disk_usage'): |
1019 ctx.cget('account.disk_usage'): |
919 order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1), |
1020 order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1), |
920 ('home', 0), ('mail_location', 0), |
1021 ('home', 0), ('mail_location', 0), |
921 ('quota storage', 0), ('quota messages', 0), |
1022 ('quota storage', 0), ('quota messages', 0), |
922 ('disk usage', 0), ('transport', 0), ('smtp', 1), |
1023 ('disk usage', 0), ('transport', 0), ('smtp', 1), |