Browse Source

settings send test mail frontend

Roman 4 weeks ago
parent
commit
964b98c22a

+ 6 - 0
Core/API/MailAPI.class.php

@@ -185,6 +185,7 @@ namespace Core\API\Mail {
             $mail->addStringAttachment("Version: 1", null, PHPMailer::ENCODING_BASE64, "application/pgp-encrypted", "");
             $mail->addStringAttachment($encryptedBody, "encrypted.asc", PHPMailer::ENCODING_7BIT, "application/octet-stream", "");
           } else {
+            $this->logger->error("Error encrypting with gpg: " . $res["error"]);
             return $this->createError($res["error"]);
           }
         } else {
@@ -237,6 +238,7 @@ namespace Core\API\Mail {
       if ($this->success && is_array($mailQueueItems)) {
         if ($debug) {
           echo "Found " . count($mailQueueItems) . " mails to send" . PHP_EOL;
+          $this->logger->debug("Found " . count($mailQueueItems) . " mails to send");
         }
 
         $successfulMails = 0;
@@ -249,6 +251,7 @@ namespace Core\API\Mail {
 
           if ($debug) {
             echo "Sending subject=$mailQueueItem->subject to=$mailQueueItem->to" . PHP_EOL;
+            $this->logger->debug("Sending subject=$mailQueueItem->subject to=$mailQueueItem->to");
           }
 
           if ($mailQueueItem->send($this->context)) {
@@ -257,6 +260,9 @@ namespace Core\API\Mail {
         }
 
         $this->success = $successfulMails === count($mailQueueItems);
+        if ($successfulMails > 0) {
+          $this->logger->debug("Sent $successfulMails emails successfully");
+        }
       }
 
       return $this->success;

+ 4 - 0
Core/API/SettingsAPI.class.php

@@ -117,6 +117,10 @@ namespace Core\API\Settings {
 
         $this->success = ($query->execute() !== FALSE);
         $this->lastError = $sql->getLastError();
+
+        if ($this->success) {
+          $this->logger->info("The site settings were changed");
+        }
       }
 
       return $this->success;

+ 12 - 4
Core/API/UserAPI.class.php

@@ -183,6 +183,8 @@ namespace Core\API\User {
 
       $groups = [];
       $sql = $this->context->getSQL();
+      $currentUser = $this->context->getUser();
+      $currentUserId = $currentUser->getId();
 
       $requestedGroups = array_unique($this->getParam("groups"));
       if (!empty($requestedGroups)) {
@@ -190,7 +192,7 @@ namespace Core\API\User {
         foreach ($requestedGroups as $groupId) {
           if (!isset($availableGroups[$groupId])) {
             return $this->createError("Group with id=$groupId does not exist.");
-          } else if ($groupId === Group::ADMIN && !$this->context->getUser()->hasGroup(Group::ADMIN)) {
+          } else if ($groupId === Group::ADMIN && !$currentUser->hasGroup(Group::ADMIN)) {
             return $this->createError("You cannot create users with administrator groups.");
           }
         }
@@ -202,6 +204,7 @@ namespace Core\API\User {
       if ($user !== false) {
         $this->user = $user;
         $this->result["userId"] = $user->getId();
+        $this->logger->info("A new user with username='$username' and email='$email' was created by userId='$currentUserId'");
       }
 
       return $this->success;
@@ -423,6 +426,9 @@ namespace Core\API\User {
         return false;
       }
 
+      $currentUserId = $this->context->getUser()->getId();
+      $this->logger->info("A new user with username='$username' and email='$email' was invited by userId='$currentUserId'");
+
       // Create Token
       $token = generateRandomString(36);
       $validDays = 7;
@@ -724,6 +730,8 @@ namespace Core\API\User {
         return false;
       }
 
+      $this->logger->info("A new user with username='$username' and email='$email' was created");
+
       $validHours = 48;
       $token = generateRandomString(36);
       $userToken = new UserToken($user, $token, UserToken::TYPE_EMAIL_CONFIRM, $validHours);
@@ -756,12 +764,12 @@ namespace Core\API\User {
           $this->lastError = $request->getLastError();
         }
       } else {
-        $this->lastError = "Could create user token: " . $sql->getLastError();
+        $this->lastError = "Could not create user token: " . $sql->getLastError();
         $this->success = false;
       }
 
       if (!$this->success) {
-        $this->logger->error("Could not deliver email to=$email type=register reason=" . $this->lastError);
+        $this->logger->error("Could not deliver email to='$email' type='register' reason='" . $this->lastError . "'");
         $this->lastError = "Your account was registered but the confirmation email could not be sent. " .
           "Please contact the server administration. This issue has been automatically logged. Reason: " . $this->lastError;
       }
@@ -1007,7 +1015,7 @@ namespace Core\API\User {
               "gpgFingerprint" => $gpgFingerprint
             ));
             $this->lastError = $request->getLastError();
-            $this->logger->info("Requested password reset for user id=" . $user->getId() . " by ip_address=" . $_SERVER["REMOTE_ADDR"]);
+            $this->logger->info("Requested password reset for user id='" . $user->getId() . "' by ip_address='" . $_SERVER["REMOTE_ADDR"] . "'");
           }
         }
       }

+ 2 - 1
Core/Configuration/Settings.class.php

@@ -159,7 +159,8 @@ class Settings {
       ->addRow("mail_password", "", true, false)
       ->addRow("mail_from", "", false, false)
       ->addRow("mail_last_sync", "", false, false)
-      ->addRow("mail_footer", "", false, false);
+      ->addRow("mail_footer", "", false, false)
+      ->addRow("mail_async", false, false, false);
   }
 
   public function getSiteName(): string {

+ 2 - 0
Core/Localization/de_DE/general.php

@@ -50,6 +50,8 @@ return [
   "info" => "Info",
   "reload" => "Aktualisieren",
   "success" => "Erfolg",
+  "send" => "Senden",
+  "sending" => "Sende ab",
 
   # file
   "choose_file" => "Datei auswählen",

+ 5 - 0
Core/Localization/de_DE/settings.php

@@ -47,6 +47,9 @@ return [
   "mail_username" => "Mail-Server Benutzername",
   "mail_password" => "Mail-Server Passwort",
   "mail_footer" => "Pfad zum E-Mail-Footer",
+  "mail_async" => "E-Mails asynchron senden (erfordert einen Cron-Job)",
+  "mail_address" => "E-Mail Adresse",
+  "send_test_email" => "Test E-Mail senden",
 
   # recaptcha
   "recaptcha_enabled" => "Aktiviere Google reCaptcha",
@@ -57,4 +60,6 @@ return [
   "fetch_settings_error" => "Fehler beim Holen der Einstellungen",
   "save_settings_success" => "Einstellungen erfolgreich gespeichert",
   "save_settings_error" => "Fehler beim Speichern der Einstellungen",
+  "send_test_email_error" => "Fehler beim Senden der Test E-Mail",
+  "send_test_email_success" => "Test E-Mail erfolgreich versendet, überprüfen Sie Ihren Posteingang!",
 ];

+ 2 - 0
Core/Localization/en_US/general.php

@@ -48,6 +48,8 @@ return [
   "info" => "Info",
   "reload" => "Reload",
   "success" => "Success",
+  "send" => "Send",
+  "sending" => "Sending",
 
   # file
   "choose_file" => "Choose File",

+ 5 - 0
Core/Localization/en_US/settings.php

@@ -47,6 +47,9 @@ return [
   "mail_username" => "Mail server username",
   "mail_password" => "Mail server password",
   "mail_footer" => "Path to e-mail footer",
+  "mail_async" => "Send e-mails asynchronously (requires a cron-job)",
+  "mail_address" => "Mail address",
+  "send_test_email" => "Send test e-mail",
 
   # recaptcha
   "recaptcha_enabled" => "Enable Google reCaptcha",
@@ -57,4 +60,6 @@ return [
   "fetch_settings_error" => "Error fetching settings",
   "save_settings_success" => "Settings saved successfully",
   "save_settings_error" => "Error saving settings",
+  "send_test_email_error" => "Error sending test email",
+  "send_test_email_success" => "Test email successfully sent. Please check your inbox!",
 ];

+ 57 - 5
react/admin-panel/src/views/settings.js

@@ -3,7 +3,7 @@ import {LocaleContext} from "shared/locale";
 import {
     Box, Button, Checkbox,
     CircularProgress, FormControl, FormControlLabel,
-    FormGroup, FormLabel, IconButton,
+    FormGroup, FormLabel, Grid, IconButton,
     Paper, Select, styled,
     Tab,
     Table,
@@ -14,7 +14,17 @@ import {
     Tabs, TextField
 } from "@mui/material";
 import {Link} from "react-router-dom";
-import {Add, Delete, Google, LibraryBooks, Mail, RestartAlt, Save, SettingsApplications} from "@mui/icons-material";
+import {
+    Add,
+    Delete,
+    Google,
+    LibraryBooks,
+    Mail,
+    RestartAlt,
+    Save,
+    Send,
+    SettingsApplications
+} from "@mui/icons-material";
 import {TableContainer} from "@material-ui/core";
 import TIME_ZONES from "shared/time-zones";
 
@@ -50,6 +60,7 @@ export default function SettingsView(props) {
           "mail_port",
           "mail_username",
           "mail_password",
+          "mail_async",
       ],
       "recaptcha": [
           "recaptcha_enabled",
@@ -69,6 +80,8 @@ export default function SettingsView(props) {
     const [hasChanged, setChanged] = useState(false);
     const [isSaving, setSaving] = useState(false);
     const [newKey, setNewKey] = useState("");
+    const [testMailAddress, setTestMailAddress] = useState("");
+    const [isSending, setSending] = useState(false);
 
     const isUncategorized = (key) => {
         return !(Object.values(KNOWN_SETTING_KEYS).reduce((acc, arr) => {
@@ -161,11 +174,26 @@ export default function SettingsView(props) {
         }
     }, [settings]);
 
+    const onSendTestMail = useCallback(() => {
+        if (!isSending) {
+            setSending(true);
+            api.sendTestMail(testMailAddress).then(data => {
+                setSending(false);
+               if (!data.success) {
+                   showDialog(data.msg, L("settings.send_test_email_error"));
+               } else {
+                   showDialog(L("settings.send_test_email_success"), L("general.success"));
+                   setTestMailAddress("");
+               }
+            });
+        }
+    }, [api, showDialog, testMailAddress, isSending]);
+
     if (settings === null) {
         return <CircularProgress />
     }
 
-    const parseBool = (v) => v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase());
+    const parseBool = (v) => v !== undefined && (v === true || v === 1 || ["true", "1", "yes"].includes(v.toString().toLowerCase()));
 
     const renderTextInput = (key_name, disabled=false, props={}) => {
         return <SettingsFormGroup key={"form-" + key_name} {...props}>
@@ -254,6 +282,30 @@ export default function SettingsView(props) {
                 renderTextInput("mail_username", !parseBool(settings.mail_enabled)),
                 renderPasswordInput("mail_password", !parseBool(settings.mail_enabled)),
                 renderTextInput("mail_footer", !parseBool(settings.mail_enabled)),
+                renderCheckBox("mail_async", !parseBool(settings.mail_enabled)),
+                <FormGroup key={"mail-test"}>
+                    <FormLabel>{L("settings.send_test_email")}</FormLabel>
+                    <FormControl disabled={!parseBool(settings.mail_enabled)}>
+                        <Grid container spacing={1}>
+                            <Grid item xs={1}>
+                                <Button startIcon={isSending ? <CircularProgress size={14} /> : <Send />}
+                                        variant={"outlined"} onClick={onSendTestMail}
+                                        fullWidth={true}
+                                        disabled={!parseBool(settings.mail_enabled) || isSending || !api.hasPermission("mail/test")}>
+                                    {isSending ? L("general.sending") + "…" : L("general.send")}
+                                </Button>
+                            </Grid>
+                            <Grid item xs={11}>
+                                <TextField disabled={!parseBool(settings.mail_enabled)}
+                                           fullWidth={true}
+                                           variant={"outlined"} value={testMailAddress}
+                                           onChange={e => setTestMailAddress(e.target.value)}
+                                           size={"small"} type={"email"}
+                                           placeholder={L("settings.mail_address")} />
+                            </Grid>
+                        </Grid>
+                    </FormControl>
+                </FormGroup>
             ];
         } else if (selectedTab === "recaptcha") {
             return [
@@ -291,7 +343,7 @@ export default function SettingsView(props) {
                             <TableRow>
                                 <TableCell>
                                     <TextField fullWidth={true} size={"small"} onChange={e => setNewKey(e.target.value)}
-                                        onBlur={() => onAddKey(newKey)} value={newKey}/>
+                                        onBlur={() => onAddKey(newKey)} value={newKey} />
                                 </TableCell>
                                 <TableCell>
                                     <TextField fullWidth={true} size={"small"} />
@@ -348,7 +400,7 @@ export default function SettingsView(props) {
             <ButtonBar>
                 <Button color={"primary"}
                         onClick={onSaveSettings}
-                        disabled={isSaving}
+                        disabled={isSaving || !api.hasPermission("settings/set")}
                         startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
                         variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}>
                     {isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))}