1: #!/usr/pkg/bin/perl
2:
3: #
4: # Portolis Mail Management Portal
5: # Copyright (C) 2025 Coherent Logic Development LLC
6: #
7: # $Id: portolis.cgi,v 1.5 2025/02/17 17:16:26 snw Exp $
8: # Author: Serena Willis <snw@coherent-logic.com>
9: #
10: # Licensed AGPL-3.0
11: #
12: # $Log: portolis.cgi,v $
13: # Revision 1.5 2025/02/17 17:16:26 snw
14: # Make password changes apply to the actual dovecot users file, though hardcoded to NetBSD paths
15: #
16: # Revision 1.4 2025/02/17 15:55:49 snw
17: # Fix password change form action URL
18: #
19: # Revision 1.3 2025/02/17 15:54:19 snw
20: # Add password change capability
21: #
22: # Revision 1.2 2025/02/16 03:14:50 snw
23: # Add background image
24: #
25: # Revision 1.1.1.1 2025/02/15 23:09:22 snw
26: # Initial commit
27: #
28: #
29:
30: use CGI qw(:standard);
31: use CGI::Session;
32:
33: $session = CGI::Session->new();
34:
35: sub init
36: {
37: $cookie = cookie(CGISESSID => $session->id );
38: print header(-cookie=>$cookie);
39: }
40:
41: sub find_alias {
42: my ($target, $alias) = @_;
43:
44: open(FH, '<', '/etc/postfix/virtual_aliases');
45:
46: while(<FH>) {
47: my @entry = split(' ', $_);
48: my $falias = $entry[0];
49: my $ftarget = $entry[1];
50:
51: if(($ftarget eq $target) && ($falias eq $alias)) {
52: close FH;
53: return 1;
54: }
55: }
56:
57: close FH;
58: return 0;
59: }
60:
61: sub add_alias {
62: my ($target, $alias) = @_;
63:
64: }
65:
66: sub vmail_auth {
67: my ($auser, $apass) = @_;
68:
69: my $line = "";
70: my @pwent = ();
71:
72: open(FH, "<", "/usr/pkg/etc/dovecot/users");
73:
74: while(<FH>){
75: $line = $_;
76: @pwent = split(':', $line);
77: if($pwent[0] eq $auser) {
78:
79: my $result = `/usr/pkg/bin/doveadm pw -t \'$pwent[1]\' -p \'$apass\'`;
80: my @res = split(' ', $result);
81:
82: if($res[1] eq "(verified)") {
83: close FH;
84: return 1;
85: }
86: }
87: }
88:
89: close FH;
90: return 0;
91: }
92:
93: sub render_header {
94: my ($title) = @_;
95:
96: my $navbar = '';
97: my $html = <<"END_HDR";
98: <HTML>
99: <HEAD>
100: <TITLE>$title</TITLE>
101: </HEAD>
102: <BODY BGCOLOR=PaleGoldenrod BACKGROUND=/images/linen2d.jpg>
103: END_HDR
104:
105: print $html;
106:
107: if($session->param("~logged-in")) {
108: my $email = $session->param("~email");
109: $navbar = <<"END_NAVL";
110: <A HREF=/cgi-bin/portolis.cgi?exec=home>$email</A> | <A HREF=https://webmail.coherent-logic.com>WebMail</A> | <A HREF=/cgi-bin/portolis.cgi?exec=pw>Change Password</A> | <A HREF=/cgi-bin/portolis.cgi?exec=logout>Log out</A>
111: <HR>
112: END_NAVL
113: }
114: else {
115: $navbar = <<"END_NAVO";
116: <A HREF=https://webmail.coherent-logic.com>WebMail</A> | <A HREF=/cgi-bin/portolis.cgi?exec=login>Log In</A>
117: <HR>
118: END_NAVO
119: }
120:
121: print $navbar;
122:
123: }
124:
125: sub render_footer {
126:
127: my $html = <<'END_FTR';
128: <HR>
129: <EM>$Id: portolis.cgi,v 1.5 2025/02/17 17:16:26 snw Exp $<BR>
130: Copyright © 2025 Coherent Logic Development LLC</EM>
131: </BODY>
132: </HTML>
133: END_FTR
134:
135: print $html;
136: }
137:
138: sub list_aliases {
139: my $email = $session->param("~email");
140: open(FH, '<', '/etc/postfix/virtual_aliases');
141:
142: print "<TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>";
143: print "<TR><TH>Alias</TH><TH>Actions</TH></TR>";
144: while(<FH>) {
145: my $ln = $_;
146: my @line = split(' ', $ln);
147:
148: if($line[1] eq $email) {
149: print "<TR><TD>$line[0]</TD><TD><A HREF=/cgi-bin/portolis.cgi?exec=delalias&id=$line[0]>Delete</A></TD></TR>";
150: }
151: }
152: print "</TABLE>";
153: print "<A HREF=/cgi-bin/portolis.cgi?exec=newalias>New Alias</A>";
154: close FH;
155: }
156:
157: sub exec_home {
158: render_header "Home";
159: my $email = $session->param("~email");
160: my $html = <<"END_HOME";
161: <CENTER>
162: <H1>Account Overview</H1>
163: <P>Your e-mail address is <B>$email</B></P>
164: END_HOME
165:
166: print $html;
167:
168: list_aliases();
169:
170: print "</CENTER>";
171: }
172:
173: sub exec_newalias {
174: my $html = '';
175:
176: if($session->param("~logged-in") == 0) {
177: render_header "Access Denied";
178: $html = <<"END_BADNAF";
179: <H1>Access Denied</H1>
180: <P>You are not logged in.</P>
181: END_BADNAF
182:
183: print $html;
184: return;
185: }
186:
187: my $email = $session->param("~email");
188: render_header "New Alias";
189:
190: if(request_method() eq 'GET') {
191: $html = <<"END_NAF";
192: <CENTER>
193: <H1>Create New Alias</H1>
194: <FORM METHOD=POST ACTION=/cgi-bin/portolis.cgi?exec=newalias>
195: <TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>
196: <TR>
197: <TD><B>Alias:</B></TD>
198: <TD><INPUT TYPE=TEXT NAME=alias></TD>
199: </TR>
200: <TR>
201: <TD><B>Target:</B></TD>
202: <TD>$email</TD>
203: </TR>
204: <TR>
205: <TD COLSPAN=2 ALIGN=RIGHT>
206: <INPUT TYPE=SUBMIT NAME=SUBMIT VALUE=Submit>
207: </TD>
208: </TR>
209: </TABLE>
210: </FORM>
211: </CENTER>
212: END_NAF
213: }
214: else {
215: my $proposed = param("alias");
216: my $result = find_alias($email, $proposed);
217:
218: if($result == 0) {
219: open(FH, '>>', '/etc/postfix/virtual_aliases');
220: flock(FH, 2);
221: print FH "$proposed\t$email\n";
222: flock(FH, 8);
223: close(FH);
224: chdir '/etc/postfix';
225: system '/usr/sbin/postmap virtual_aliases';
226: $html = <<"END_AAOK";
227: <CENTER>
228: <H1>Alias Added</H1>
229: <A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
230: </CENTER>
231: END_AAOK
232: }
233: else {
234: $html = <<"END_AADUP";
235: <CENTER>
236: <H1>Duplicate Alias</H1>
237: <P>The alias <B>$proposed</B> already exists</P>
238: <A HREF=/cgi-bin/portolis.cgi?exec=newalias>Try Again</a>
239: </CENTER>
240: END_AADUP
241: }
242: }
243:
244: print $html;
245:
246: }
247:
248: sub exec_delalias {
249: my $alias = url_param('id');
250: my $html = '';
251:
252: if($session->param("~logged-in") == 0) {
253: render_header "Access Denied";
254: $html = <<"END_BADDAF";
255: <H1>Access Denied</H1>
256: <P>You are not logged in.</P>
257: END_BADDAF
258:
259: print $html;
260: return;
261: }
262:
263: my $email = $session->param("~email");
264: render_header "Delete Alias";
265:
266: if(request_method() eq 'GET') {
267: if(find_alias($email, $alias) == 1) {
268: my @lines = ();
269:
270: open(FH, '<', '/etc/postfix/virtual_aliases');
271: flock(FH, 2);
272: while(<FH>) {
273: my $line = $_;
274: my @tmp = split(' ', $line);
275: if($tmp[0] ne $alias) {
276: push @lines, $line;
277: }
278: if(($tmp[0] eq $alias) && ($tmp[1] ne $email)) {
279: push @lines, $line;
280: }
281: }
282: flock(FH, 8);
283: close(FH);
284: open(FH, '>', '/etc/postfix/virtual_aliases');
285: flock(FH, 2);
286: seek(FH, 0, 0);
287: truncate(FH, 0);
288: print FH @lines;
289: flock(FH, 8);
290: close FH;
291: chdir '/etc/postfix';
292: system '/usr/sbin/postmap virtual_aliases';
293: $html = <<"END_DAGOOD";
294: <CENTER>
295: <H1>Alias Deleted</H1>
296: <P><A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
297: </CENTER>
298: END_DAGOOD
299: }
300: else {
301: $html = <<"END_DANOF";
302: <CENTER>
303: <H1>Invalid Alias</H1>
304: <P><A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
305: </CENTER>
306: END_DANOF
307: }
308: }
309:
310: print $html;
311:
312: }
313:
314: sub exec_pw {
315:
316: if($session->param("~logged-in") == 0) {
317: render_header "Access Denied";
318: $html = <<"END_BADDAF";
319: <H1>Access Denied</H1>
320: <P>You are not logged in.</P>
321: END_BADDAF
322:
323: print $html;
324: return;
325: }
326:
327: my $html = '';
328: render_header "Change Password";
329:
330: if(request_method() eq 'GET') {
331: $html = <<"END_EPW";
332: <CENTER>
333: <H1>Change Password</H1>
334: <FORM METHOD=POST ACTION=/cgi-bin/portolis.cgi?exec=pw>
335: <TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>
336: <TR>
337: <TD><B>Password:</B></TD>
338: <TD><INPUT TYPE=PASSWORD NAME=password></TD>
339: </TR>
340: <TR>
341: <TD><B>Enter again to confirm:</B></TD>
342: <TD><INPUT TYPE=PASSWORD NAME=password_confirm></TD>
343: </TR>
344: <TR>
345: <TD COLSPAN=2 ALIGN=RIGHT>
346: <INPUT TYPE=SUBMIT NAME=submit VALUE=Submit>
347: </TD>
348: </TR>
349: </TABLE>
350: </FORM>
351: END_EPW
352: }
353: elsif(request_method() eq 'POST') {
354: my $password = param('password');
355: my $password_confirm = param('password_confirm');
356: my $email = $session->param("~email");
357: my @parts = split('@', $email);
358: my $localpart = $parts[0];
359: my $domainpart = $parts[1];
360: if($password eq $password_confirm) {
361: my $hash = `/usr/pkg/bin/doveadm pw -p \"$password\"`;
362: $hash =~s/\R//g;
363: my $str = "$email:$hash:1007:1007:/home/maildeliverer/$domainpart/$localpart\n";
364:
365: open(FH, '<', '/usr/pkg/etc/dovecot/users');
366: flock FH, 2;
367:
368: my @lines = ();
369:
370: while(<FH>) {
371: my $line = $_;
372: my @pwent = split(':', $line);
373: if($pwent[0] ne $email) {
374: push @lines, $line;
375: }
376: }
377: push @lines, $str;
378:
379: flock FH, 8;
380: close FH;
381:
382: open(FH, '>', '/usr/pkg/etc/dovecot/users');
383: flock(FH, 2);
384: seek(FH, 0, 0);
385: truncate(FH, 0);
386: print FH @lines;
387: flock FH, 8;
388: close(FH);
389: $html = <<"END_PWCPOST";
390: <CENTER>
391: <H1>Password Changed</H1>
392: <A HREF=/cgi-bin/portolis.cgi?exec=home>Home</A>
393: </CENTER>
394: END_PWCPOST
395:
396: }
397: else {
398: $html = <<"END_PWCNM";
399: <CENTER>
400: <H1>Submission Error</H1>
401: <P>Passwords did not match. <A HREF=/cgi-bin/portolis.cgi?exec=pw>Try again</A>.</P>
402: </CENTER>
403: END_PWCNM
404: }
405: }
406:
407: print $html;
408:
409: }
410:
411: sub exec_login {
412:
413: render_header "Login";
414:
415: my $html = '';
416:
417: if(request_method() eq 'GET') {
418: $html = <<"END_LOGIN";
419: <CENTER>
420: <H1>Log In</H1>
421: <FORM METHOD=POST ACTION=/cgi-bin/portolis.cgi?exec=login>
422: <TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>
423: <tr>
424: <td><b>E-Mail Address:</b></td>
425: <td><input type="text" name="email"></td>
426: </tr>
427: <tr>
428: <td><b>Password:</b></td>
429: <td><input type="password" name="password"></td>
430: </tr>
431: <tr>
432: <td colspan=2 align=right><input type=submit name=submit value=Submit></td>
433: </tr>
434: </TABLE>
435: </FORM>
436: </CENTER>
437: END_LOGIN
438: }
439: elsif(request_method() eq 'POST') {
440: my $email = param("email");
441: my $password = param("password");
442:
443: if(vmail_auth($email, $password) == 1) {
444: $session->param("~logged-in", 1);
445: $session->param("~email", $email);
446: $html = <<'END_LOGINDONE';
447: <script>location.replace('/cgi-bin/portolis.cgi?exec=home');</script>
448:
449: END_LOGINDONE
450: }
451: else {
452: $html = <<'END_ACD';
453: <H1>Access Denied</H1>
454: <P>Invalid e-mail address or password</P>
455: END_ACD
456: }
457: }
458:
459: print $html;
460:
461:
462: }
463:
464: sub exec_logout {
465: $session->clear(['~logged-in', 'email']);
466: my $html = <<'END_LO';
467: <script>location.replace('/cgi-bin/portolis.cgi?exec=login');</script>
468: END_LO
469: print $html;
470: }
471:
472: sub exec_unknown {
473:
474: render_header "Error";
475:
476: my $html = <<'END_UNK';
477: <CENTER>
478: <H1>Invalid Request</H1>
479: <P>The action you're attempting is invalid or has not yet been implemented. Please try something else.</P>
480: </CENTER>
481: END_UNK
482:
483: print $html;
484:
485: }
486:
487: sub main {
488: init;
489:
490: my $exec = url_param('exec');
491:
492: if($exec ne '') {
493: if($exec eq 'login') {
494: $funcref = \&exec_login;
495: }
496: elsif($exec eq 'logout') {
497: $funcref = \&exec_logout;
498: }
499: elsif($exec eq 'home') {
500: $funcref = \&exec_home;
501: }
502: elsif($exec eq 'newalias') {
503: $funcref = \&exec_newalias;
504: }
505: elsif($exec eq 'delalias') {
506: $funcref = \&exec_delalias;
507: }
508: elsif($exec eq 'pw') {
509: $funcref = \&exec_pw;
510: }
511: else {
512: $funcref = \&exec_unknown;
513: }
514: }
515: else {
516: $funcref = \&exec_login;
517: }
518:
519: &$funcref;
520:
521: render_footer;
522: }
523:
524: main();
525:
526:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>