diff --git a/server/notification-providers/ooredoo.js b/server/notification-providers/ooredoo.js
new file mode 100644
index 000000000..32985bb0d
--- /dev/null
+++ b/server/notification-providers/ooredoo.js
@@ -0,0 +1,89 @@
+const NotificationProvider = require("./notification-provider");
+const axios = require("axios");
+
+// The Ooredoo bulk_sms/v2 gateway accepts at most this many recipients per
+// request, so larger recipient lists are split into batches of this size.
+const MAX_RECIPIENTS_PER_REQUEST = 20;
+
+class Ooredoo extends NotificationProvider {
+ name = "Ooredoo";
+
+ /**
+ * @inheritdoc
+ */
+ async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+ const okMsg = "Sent Successfully.";
+ const url = notification.ooredooServerUrl || "https://o-papi1-lb01.ooredoo.mv/bulk_sms/v2";
+
+ // Users may enter recipients separated by comma, semicolon, space or
+ // newline.
+ const recipients = notification.ooredooToNumber
+ .split(/[\s,;]+/)
+ .map((number) => this.normalizePhoneNumber(number))
+ .filter((number) => number !== "");
+
+ if (recipients.length === 0) {
+ throw new Error("No valid recipient phone number was provided.");
+ }
+
+ try {
+ let config = {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Authorization: "Bearer " + notification.ooredooBearerToken,
+ },
+ };
+ config = this.getAxiosConfigWithProxy(config);
+
+ // The gateway only accepts MAX_RECIPIENTS_PER_REQUEST numbers per
+ // call, so send the recipients in batches of that size.
+ for (let i = 0; i < recipients.length; i += MAX_RECIPIENTS_PER_REQUEST) {
+ const batch = recipients.slice(i, i + MAX_RECIPIENTS_PER_REQUEST);
+ const data = new URLSearchParams({
+ username: notification.ooredooUsername,
+ // The gateway expects the access key to be Base64 encoded.
+ access_key: Buffer.from(notification.ooredooAccessKey).toString("base64"),
+ message: msg,
+ batch: batch.join(" "),
+ });
+
+ const resp = await axios.post(url, data.toString(), config);
+
+ // The gateway returns HTTP 200 even on failure; the real outcome
+ // is carried in "response_code" (0 means the batch was accepted).
+ if (!resp.data || Number(resp.data.response_code) !== 0) {
+ const reason =
+ resp.data && resp.data.response_message
+ ? resp.data.response_message
+ : "response_code=" + (resp.data ? resp.data.response_code : "unknown");
+ throw new Error("Ooredoo rejected the message: " + reason);
+ }
+ }
+
+ return okMsg;
+ } catch (error) {
+ this.throwGeneralAxiosError(error);
+ }
+ }
+
+ /**
+ * Normalize a Maldivian phone number to the "960XXXXXXX" format expected
+ * by the Ooredoo gateway. Numbers already in that form are returned as is.
+ * @param {string} phoneNumber The phone number to normalize
+ * @returns {string} The normalized phone number, or "" if it is empty
+ */
+ normalizePhoneNumber(phoneNumber) {
+ const number = phoneNumber.replace(/[\s+]/g, "");
+
+ if (number === "") {
+ return "";
+ }
+ // A bare 7-digit local number gets the Maldives (960) country code.
+ if (/^\d{7}$/.test(number)) {
+ return "960" + number;
+ }
+ return number;
+ }
+}
+
+module.exports = Ooredoo;
diff --git a/server/notification.js b/server/notification.js
index 9cd327047..fb8a1bebc 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -37,6 +37,7 @@ const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneChat = require("./notification-providers/onechat");
const OneBot = require("./notification-providers/onebot");
+const Ooredoo = require("./notification-providers/ooredoo");
const Opsgenie = require("./notification-providers/opsgenie");
const JiraServiceManagement = require("./notification-providers/jira-service-management");
const PagerDuty = require("./notification-providers/pagerduty");
@@ -150,6 +151,7 @@ class Notification {
new OneChat(),
new OneBot(),
new Onesender(),
+ new Ooredoo(),
new Opsgenie(),
new JiraServiceManagement(),
new PagerDuty(),
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue
index 4fa6d5a32..57ae408f8 100644
--- a/src/components/NotificationDialog.vue
+++ b/src/components/NotificationDialog.vue
@@ -324,6 +324,7 @@ export default {
Feishu: "Feishu (飞书)",
FlashDuty: "FlashDuty (快猫星云)",
FreeMobile: "FreeMobile (mobile.free.fr)",
+ Ooredoo: "Ooredoo (Maldives)",
PushDeer: "PushDeer",
promosms: "PromoSMS",
serwersms: "SerwerSMS.pl",
diff --git a/src/components/notifications/Ooredoo.vue b/src/components/notifications/Ooredoo.vue
new file mode 100644
index 000000000..c90a0db20
--- /dev/null
+++ b/src/components/notifications/Ooredoo.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
{{ $t("ooredooUsernameDescription") }}
+
+
+
+
+
{{ $t("ooredooAccessKeyDescription") }}
+
+
+
+
+
{{ $t("ooredooBearerTokenDescription") }}
+
+
+
+
+
{{ $t("ooredooRecipientDescription") }}
+
+
+
+
+
{{ $t("Leave blank to use the default Ooredoo endpoint.") }}
+
+
+
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js
index ec31a77db..6f7f31a48 100644
--- a/src/components/notifications/index.js
+++ b/src/components/notifications/index.js
@@ -37,6 +37,7 @@ import Octopush from "./Octopush.vue";
import OneChat from "./OneChat.vue";
import OneBot from "./OneBot.vue";
import Onesender from "./Onesender.vue";
+import Ooredoo from "./Ooredoo.vue";
import Opsgenie from "./Opsgenie.vue";
import JiraServiceManagement from "./JiraServiceManagement.vue";
import PagerDuty from "./PagerDuty.vue";
@@ -138,6 +139,7 @@ const NotificationFormList = {
OneChat: OneChat,
OneBot: OneBot,
Onesender: Onesender,
+ Ooredoo: Ooredoo,
Opsgenie: Opsgenie,
JiraServiceManagement: JiraServiceManagement,
PagerDuty: PagerDuty,
diff --git a/src/lang/en.json b/src/lang/en.json
index 9c538de25..e45502382 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -957,6 +957,12 @@
"smspartnerSenderName": "SMS Sender Name",
"smspartnerSenderNameInfo": "Must be between 3..=11 regular characters",
"Recipient Number": "Recipient Number",
+ "Access Key": "Access Key",
+ "ooredooUsernameDescription": "The email address you use to sign in to the Ooredoo bulk SMS dashboard.",
+ "ooredooAccessKeyDescription": "Found under \"API Access Key\" on your Ooredoo SMS dashboard. Paste it exactly as shown — it is base64-encoded automatically before sending.",
+ "ooredooBearerTokenDescription": "The token shown after \"Authorization: Bearer\" in the sample cURL command on your Ooredoo SMS dashboard.",
+ "ooredooRecipientDescription": "Enter one or more numbers separated by a comma or space. Local 7-digit numbers are automatically prefixed with 960.",
+ "Leave blank to use the default Ooredoo endpoint.": "Leave blank to use the default Ooredoo endpoint.",
"From Name/Number": "From Name/Number",
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
"Octopush API Version": "Octopush API Version",