summaryrefslogtreecommitdiff
path: root/localmkpostfixvirtual
blob: 7983197d2b3fbd0a2513243e65c624ddb845ff2b (plain)
  1. #!/usr/bin/perl -wT
  2. #
  3. # /usr/local/sbin/localmkpostfixvirtual
  4. # Copyright 2001-2006 Jonas Smedegaard <dr@jones.dk>
  5. #
  6. # $Id: localmkpostfixvirtual,v 1.34 2006-10-30 13:36:49 jonas Exp $
  7. #
  8. # Generate virtual file for postfix
  9. #
  10. # This script provides a poor-man's email ISP tool using the standard
  11. # Unix users/groups database (with generic getent lookups, supporting
  12. # smooth upgrades to LDAP or other enterprise systems supporting the
  13. # libc Name Service Switch.
  14. #
  15. #
  16. # Email domains
  17. # -------------
  18. #
  19. # A system group is needed to locate email domains:
  20. #
  21. # # addgroup --system maildomains
  22. #
  23. #
  24. # Each domain (or group of syncronously maintained domains) needs a
  25. # virtual user, added to that central group:
  26. #
  27. # # adduser --system --no-create-home --group --disabled-password abcde
  28. #
  29. # # adduser abcde maildomains
  30. #
  31. # In the "Other" field of the GECOS info, the virtual user accounts has
  32. # hint(s) on the email domains tied to that domain:
  33. #
  34. # # usermod -c ",,,,@abc.com @cde.org" abcde
  35. #
  36. # The GECOS info has limited space. A domain group can span multiple
  37. # virtual accounts by mentioning the secondary account names in the
  38. # group field of the main virtual account.
  39. # "Office" or "roomnumber" field of the primary mailgroup (include the
  40. # primary mailgroup itself too, if using this filed at all!).
  41. #
  42. #
  43. # Email addresses
  44. # ---------------
  45. #
  46. # In the "Other" field of the GECOS info, real user accounts has hint(s)
  47. # on the email userparts tied to that user:
  48. #
  49. # # usermod -c "db@ dbowie@abcde +@xyz" david
  50. #
  51. #
  52. # Suggestion: Add postmaster-specific hints to the root account
  53. # (default: "postmaster@"):
  54. #
  55. # # usermod -c "postmaster@ abuse@ support@abcde" root
  56. #
  57. #
  58. # Postfix lookup tables
  59. # ---------------------
  60. #
  61. # Lookup table for postfix is generated like this:
  62. #
  63. # # localmaildomaincreate
  64. #
  65. # ..and if satisified, apply the new file to the running system:
  66. #
  67. # # localmaildomainupdate
  68. #
  69. #
  70. # The above example entries should generate the following in
  71. # /etc/postfix/virtual:
  72. #
  73. # abc.com VIRTUAL
  74. # postmaster@abc.com root
  75. # abuse@abc.com root
  76. # support@abc.com root
  77. #
  78. # db@abc.com david
  79. # dbowie@abc.com david
  80. #
  81. # cde.org VIRTUAL
  82. # postmaster@cde.org root
  83. # abuse@cde.org root
  84. # support@cde.org root
  85. #
  86. # db@cde.org david
  87. # dbowie@cde.org david
  88. #
  89. # If another virtual domain account "xyz" is added with @xyz.net
  90. # in comment field, the following will be included as well:
  91. #
  92. # xyz.net VIRTUAL
  93. # postmaster@xyz.net
  94. # abuse@xyz.net
  95. #
  96. # db@xyz.net
  97. # @xyz.net
  98. #
  99. # (notice how support is only tied to the abcde domains, and the
  100. # specific dbowie userpart is substituted with a wildcard)
  101. use strict;
  102. use User::pwent;
  103. use User::grent;
  104. my (%username, %fullname, %office, %workphone, %homephone, %other);
  105. my (%username_by_gid, %addresshints, %domains);
  106. while (my $pw = getpwent()) {
  107. $username_by_gid{$pw->gid} = $pw->name;
  108. ($fullname{$pw->name}, $office{$pw->name}, $workphone{$pw->name}, $homephone{$pw->name}, $other{$pw->name}) = split /\s*,\s*/, $pw->gecos;
  109. if (defined($other{$pw->name})) {
  110. @{$addresshints{$pw->name}} = grep {/^([\.[:alnum:]_-]+|\+)?@([\.[:alnum:]_-]+)?$/} split /\s+/, $other{$pw->name};
  111. }
  112. }
  113. my (%owner, %members, %groups);
  114. while (my $gr = getgrent()) {
  115. $owner{$gr->name} = $username_by_gid{$gr->gid};
  116. $members{$gr->name} = $gr->members;
  117. foreach my $member (@{$members{$gr->name}}) {
  118. push @{$groups{$member}}, $gr->name unless (defined($owner{$gr->name}) and $member eq $owner{$gr->name});
  119. }
  120. }
  121. my (%warned);
  122. sub warnonce($$$) {
  123. my ($test, $id, $warning) = @_;
  124. if ($test) {
  125. return 1;
  126. }
  127. if ($warned{$id}) {
  128. return '';
  129. }
  130. print STDERR 'W: ' . $warning . "\n";
  131. $warned{$id} = 1;
  132. return '';
  133. }
  134. my (%username_by_localpart_by_maildomain);
  135. sub print_accounts($$$$) {
  136. my ($username, $mailgroup, $maildomain, $pre_text, $post_fallback_text) = @_;
  137. ($pre_text) && print $pre_text . "\n";
  138. my $joker_seen;
  139. if (&warnonce(defined(@{$addresshints{$username}}), "addresshints_$username", "Skipping non-hinted username \"$username\".")) {
  140. my @localparthints = @{$addresshints{$username}};
  141. my @localparts = grep {s/(.+)@($mailgroup|$maildomain)?$/$1/} @localparthints;
  142. foreach my $localpart (@localparts) {
  143. for ($localpart) {
  144. # FIXME: the below doesn't work as intended?!?
  145. # if (&warnonce(! defined(@{$username_by_localpart_by_maildomain{$_}}), "address_$_", "Skipping duplicate address \"$_\" for \"$username\").")) {
  146. # next;
  147. # }
  148. if (/^\+$/) {
  149. if (!$joker_seen) {
  150. print "\@$maildomain $username\n";
  151. $joker_seen = $username;
  152. } else {
  153. print "#WARNING: Catch-all for $maildomain already set to $joker_seen!";
  154. }
  155. } else {
  156. print "$localpart\@$maildomain $username\n";
  157. }
  158. }
  159. }
  160. } else {
  161. print $post_fallback_text . "\n" if ($post_fallback_text);
  162. }
  163. print "\n";
  164. }
  165. sub usercomment($) {
  166. my $user = shift;
  167. my @s = ();
  168. if (&warnonce(defined($fullname{$user}), "fullname_$user", "User \"$user\" lacks fullname.")) {
  169. push @s, $fullname{$user};
  170. }
  171. if (&warnonce(defined(@{$groups{$user}}), "groups_$user", "User \"$user\" belongs to no (secondary) group.")) {
  172. my @groups_sorted = sort @{$groups{$user}};
  173. push @s, '(' . join(' ', @groups_sorted) . ')';
  174. }
  175. if (@s) {
  176. unshift @s, '#';
  177. }
  178. my $string = join(' ', @s);
  179. return "$string";
  180. }
  181. my $loop;
  182. my @mailgroups = @ARGV ? @ARGV : @{$members{'maildomains'}};
  183. foreach my $mailgroup (@mailgroups) {
  184. if (not &warnonce(defined(@{$addresshints{$mailgroup}}), "addresshints_$mailgroup", "Skipping empty mailgroup \"$mailgroup\".")) {
  185. next;
  186. }
  187. my @maildomainhints = @{$addresshints{$mailgroup}};
  188. my @maildomains = grep {s/^@(.+)/$1/} @maildomainhints;
  189. foreach my $maildomain (@maildomains) {
  190. my (@mailgroupowners, @mailusers);
  191. my @mailgroupgroups = split / +/, $office{$mailgroup};
  192. push @mailgroupgroups, $mailgroup unless (@mailgroupgroups);
  193. foreach my $mailgroupgroup (@mailgroupgroups) {
  194. push @mailgroupowners, $owner{$mailgroupgroup} if ($owner{$mailgroupgroup});
  195. push @mailusers, @{$members{$mailgroupgroup}};
  196. }
  197. my @mailgroupowners_sorted = sort @mailgroupowners;
  198. my @mailusers_sorted = sort @mailusers;
  199. if ($loop) {
  200. print "\n##################################################################\n\n";
  201. }
  202. $loop++;
  203. &print_accounts('root', $mailgroup, $maildomain, "$maildomain VIRTUAL", "postmaster\@$maildomain root");
  204. # Do mailgroup owners (and don't warn if there's no addresses attached)
  205. foreach my $mailgroupowner (@mailgroupowners_sorted) {
  206. &print_accounts($mailgroupowner, $mailgroup, $maildomain, &usercomment($mailgroupowner));
  207. }
  208. # Do secondary mailgroup members
  209. foreach my $mailuser (@mailusers_sorted) {
  210. &print_accounts($mailuser, $mailgroup, $maildomain, &usercomment($mailuser), "#WARNING: No addresses for $mailuser");
  211. }
  212. }
  213. }