Adding --count-only option to mailman members command
[mailman.git] / src / mailman / commands / cli_members.py
blob5be2b176277927bd40d9e94dbd6a26f8b6d9edf1
1 # Copyright (C) 2009-2022 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 """The 'members' subcommand."""
20 import click
22 from email.utils import formataddr
23 from mailman.core.i18n import _
24 from mailman.interfaces.command import ICLISubCommand
25 from mailman.interfaces.listmanager import IListManager
26 from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole
27 from mailman.utilities.options import I18nCommand
28 from operator import attrgetter
29 from public import public
30 from zope.component import getUtility
31 from zope.interface import implementer
34 def display_members(ctx, mlist, role, regular, digest,
35 nomail, outfp, email_only, count_only):
36 # Which type of digest recipients should we display?
37 if digest == 'any':
38 digest_types = [
39 DeliveryMode.plaintext_digests,
40 DeliveryMode.mime_digests,
41 DeliveryMode.summary_digests,
43 elif digest == 'mime':
44 # Include summary with mime as they are currently treated alike.
45 digest_types = [
46 DeliveryMode.mime_digests,
47 DeliveryMode.summary_digests,
49 elif digest is not None:
50 digest_types = [DeliveryMode[digest + '_digests']]
51 else:
52 # Don't filter on digest type.
53 pass
54 # Which members with delivery disabled should we display?
55 if nomail is None:
56 # Don't filter on delivery status.
57 pass
58 elif nomail == 'byadmin':
59 status_types = [DeliveryStatus.by_moderator]
60 elif nomail.startswith('by'):
61 status_types = [DeliveryStatus['by_' + nomail[2:]]]
62 elif nomail == 'enabled':
63 status_types = [DeliveryStatus.enabled]
64 elif nomail == 'unknown':
65 status_types = [DeliveryStatus.unknown]
66 elif nomail == 'any':
67 status_types = [
68 DeliveryStatus.by_user,
69 DeliveryStatus.by_bounces,
70 DeliveryStatus.by_moderator,
71 DeliveryStatus.unknown,
73 else: # pragma: nocover
74 # click should enforce a valid nomail option.
75 raise AssertionError(nomail)
76 # Which roles should we display?
77 if role is None:
78 # By default, filter on members.
79 roster = mlist.members
80 elif role == 'administrator':
81 roster = mlist.administrators
82 elif role == 'any':
83 roster = mlist.subscribers
84 else:
85 # click should enforce a valid member role.
86 roster = mlist.get_roster(MemberRole[role])
87 # Print; outfp will be either the file or stdout to print to.
88 addresses = list(roster.addresses)
89 if len(addresses) == 0:
90 print(0 if count_only else _('${mlist.list_id} has no members'),
91 file=outfp)
92 return
93 if count_only:
94 print(roster.member_count, file=outfp)
95 return
96 for address in sorted(addresses, key=attrgetter('email')):
97 member = roster.get_member(address.email)
98 if regular:
99 if member.delivery_mode != DeliveryMode.regular:
100 continue
101 if digest is not None:
102 if member.delivery_mode not in digest_types:
103 continue
104 if nomail is not None:
105 if member.delivery_status not in status_types:
106 continue
107 if email_only:
108 print(address.original_email, file=outfp)
109 else:
110 print(formataddr((address.display_name, address.original_email)),
111 file=outfp)
114 @click.command(
115 cls=I18nCommand,
116 help=_("""\
117 Display a mailing list's members.
118 Filtering along various criteria can be done when displaying.
119 With no options given, displaying mailing list members
120 to stdout is the default mode.
121 """))
122 @click.option(
123 '--add', '-a', 'add_infp', metavar='FILENAME',
124 type=click.File(encoding='utf-8'),
125 help=_("""\
126 [MODE] Add all member addresses in FILENAME. This option is removed.
127 Use 'mailman addmembers' instead."""))
128 @click.option(
129 '--delete', '-x', 'del_infp', metavar='FILENAME',
130 type=click.File(encoding='utf-8'),
131 help=_("""\
132 [MODE] Delete all member addresses found in FILENAME.
133 This option is removed. Use 'mailman delmembers' instead."""))
134 @click.option(
135 '--sync', '-s', 'sync_infp', metavar='FILENAME',
136 type=click.File(encoding='utf-8'),
137 help=_("""\
138 [MODE] Synchronize all member addresses of the specified mailing list
139 with the member addresses found in FILENAME.
140 This option is removed. Use 'mailman syncmembers' instead."""))
141 @click.option(
142 '--output', '-o', 'outfp', metavar='FILENAME',
143 type=click.File(mode='w', encoding='utf-8', atomic=True),
144 help=_("""\
145 Display output to FILENAME instead of stdout. FILENAME
146 can be '-' to indicate standard output."""))
147 @click.option(
148 '--role', '-R',
149 type=click.Choice(('any', 'owner', 'moderator', 'nonmember', 'member',
150 'administrator')),
151 help=_("""\
152 Display only members with a given ROLE.
153 The role may be 'any', 'member', 'nonmember', 'owner', 'moderator',
154 or 'administrator' (i.e. owners and moderators).
155 If not given, then 'member' role is assumed."""))
156 @click.option(
157 '--regular', '-r',
158 is_flag=True, default=False,
159 help=_("""\
160 Display only regular delivery members."""))
161 @click.option(
162 '--email-only', '-e', 'email_only',
163 is_flag=True, default=False,
164 help=("""\
165 Display member addresses only, without the display name.
166 """))
167 @click.option(
168 '--count-only', '-c', 'count_only',
169 is_flag=True, default=False,
170 help=("""\
171 Display members count only.
172 """))
173 @click.option(
174 '--no-change', '-N', 'no_change',
175 is_flag=True, default=False,
176 help=_("""\
177 This option has no effect. It exists for backwards compatibility only."""))
178 @click.option(
179 '--digest', '-d', metavar='kind',
180 # baw 2010-01-23 summary digests are not really supported yet.
181 type=click.Choice(('any', 'plaintext', 'mime')),
182 help=_("""\
183 Display only digest members of kind.
184 'any' means any digest type, 'plaintext' means only plain text (rfc 1153)
185 type digests, 'mime' means MIME type digests."""))
186 @click.option(
187 '--nomail', '-n', metavar='WHY',
188 type=click.Choice(('enabled', 'any', 'unknown',
189 'byadmin', 'byuser', 'bybounces')),
190 help=_("""\
191 Display only members with a given delivery status.
192 'enabled' means all members whose delivery is enabled, 'any' means
193 members whose delivery is disabled for any reason, 'byuser' means
194 that the member disabled their own delivery, 'bybounces' means that
195 delivery was disabled by the automated bounce processor,
196 'byadmin' means delivery was disabled by the list
197 administrator or moderator, and 'unknown' means that delivery was disabled
198 for unknown (legacy) reasons."""))
199 @click.argument('listspec')
200 @click.pass_context
201 def members(ctx, add_infp, del_infp, sync_infp, outfp,
202 role, regular, no_change, digest, nomail, listspec,
203 email_only, count_only):
204 mlist = getUtility(IListManager).get(listspec)
205 if mlist is None:
206 ctx.fail(_('No such list: ${listspec}'))
207 if add_infp is not None:
208 ctx.fail('The --add option is removed. '
209 'Use `mailman addmembers` instead.')
210 elif del_infp is not None:
211 ctx.fail('The --delete option is removed. '
212 'Use `mailman delmembers` instead.')
213 elif sync_infp is not None:
214 ctx.fail('The --sync option is removed. '
215 'Use `mailman syncmembers` instead.')
216 elif role == 'any' and (regular or digest or nomail):
217 ctx.fail('The --regular, --digest and --nomail options are '
218 'incompatible with role=any.')
219 elif email_only and count_only:
220 ctx.fail('The --email_only and --count_only options are '
221 'mutually exclusive.')
222 else:
223 display_members(ctx, mlist, role, regular,
224 digest, nomail, outfp, email_only, count_only)
227 @public
228 @implementer(ICLISubCommand)
229 class Members:
230 name = 'members'
231 command = members