OJS 3.1.1 and DMARC?

We host many journals with editors from many different external organisations so we have set in config.inc.php:

default_envelope_sender = noreply@our.domain
force_default_envelope_sender = On

This results in the envelope sender (MAIL FROM/Return-Path) being set to default_envelope_sender, but the From: header is still set to the address of the editor, which is often a domain that we are not authorized to send mail on behalf of.
We are now getting complaints that this is in violation of DMARC and I’m told that to fix it the envelope address and the From: header address must match. I’m not sure if that is a correct interpretation of DMARC rules, but the DMARC FAQ seems to suggest that in our case it would work since we are authorized to send mail on behalf of the default_envelope_sender domain.

To that end I’ve done this modification to Mail.inc.php:

diff --git a/lib/pkp/classes/mail/Mail.inc.php b/lib/pkp/classes/mail/Mail.inc.php
index 207f56da8..c809ed986 100644
--- a/lib/pkp/classes/mail/Mail.inc.php
+++ b/lib/pkp/classes/mail/Mail.inc.php
@@ -488,7 +488,14 @@ class Mail extends DataObject {
                if (($s = $this->getEnvelopeSender()) != null) $mailer->Sender = $s;
                if (($f = $this->getFrom()) != null) {
-                       $mailer->SetFrom($f['email'], $f['name']);
+                       # If we have forced the envelope sender, then add the original from to the Reply-To header
+                       # and override the From: header with the envelope sender.
+                       if (Config::getVar('email', 'force_default_envelope_sender') && Config::getVar('email', 'default_envelope_sender')) {
+                               $mailer->AddReplyTo($f['email'], $f['name']);
+                               $mailer->SetFrom(Config::getVar('email', 'default_envelope_sender'), '');
+                       } else {
+                               $mailer->SetFrom($f['email'], $f['name']);
+                       }
                if (($r = $this->getReplyTo()) != null) {
                        $mailer->AddReplyTo($r['email'], $r['name']);

This change was inspired by pkp/pkp-lib#1723: force_default_envelope_sender moves the traditional from to a reply-to for DMARC compliance by ctgraham · Pull Request #1724 · pkp/pkp-lib · GitHub and it seems to be working in my limited testing in that it shifts the contents of the From header to a Reply-To header and then sets the From header to the default_envelope_sender.

Thoughts? Is this the right way to go?

Here is my current understanding:

DMARC is a framework which describes to the recipient Mail Transit Authority (MTA) how to deal with violations of DKIM and SPF.

DKIM digitally signs a message to declare authorship. That is, validating that the SMTP headers are accurate for the actual sender.

SPF publishes for a domain what hosts are allowed to send mail “from” the domain.

This is muddied by distinctions between the MAIL FROM (envelope sender), the SMTP From, SMTP Sender, and SMTP Return-Path. It is the MAIL FROM (envelope sender) which is compared for the purposes of SPF (and which is probably used for DKIM). The MAIL FROM becomes the SMTP Sender header.

The SMTP Sender header may differ from the SMTP From. The typical use-case is a mailing list, where the Sender is the list, but the message is coming From a list member. I think OJS/OMP usage should parallel this.

Currently, OJS/OMP does not manage the From/Sender distinction, and does not incorporate DKIM within the sending code.

My current workaround is this:

  • Implement OpenDKIM at the server (or MTA) level. I added a directive in /etc/opendkim.conf of SenderHeaders Sender,From to ensure that Sender is signed if there is a discrepancy between the Sender and From.
  • Set default_envelope_sender to an SPF trusted address (probably a noreply), and then force_default_envelope_sender. (SPF will also need to be setup outside of OJS/OMP).
  • Modify the pkp-lib to add the default_envelope_sender when force_default_envelope_sender is in effect as the SMTP Sender header. Add a Reply-To header of the From address, just for good measure. (This needs to be an “add” not “replace”, in case there are already reply-tos.)

I welcome other folks reviewing, testing, and critiquing this approach.

This is the patch against 2.4.x:

$ git diff
diff --git a/classes/mail/Mail.inc.php b/classes/mail/Mail.inc.php
index 65a910f..d8b70a5 100644
--- a/classes/mail/Mail.inc.php
+++ b/classes/mail/Mail.inc.php
@@ -336,7 +336,7 @@ class Mail extends DataObject {
        function setReplyTo($email, $name = '') {
                if ($email === null) $this->setData('replyTo', null);
-               return $this->setData('replyTo', array('name' => $name, 'email' => $email));
+               return $this->setData('replyTo', array(array('name' => $name, 'email' => $email)));

@@ -348,16 +348,32 @@ class Mail extends DataObject {

+        * Add a reply-to for the message.
+        * @param $email string
+        * @param $name string optional
+        */
+       function addReplyTo($email, $name = '') {
+               if ($email === null) return false;
+               $replyTo = $this->getReplyTo();
+               if (!is_array($replyTo)) {
+                       $replyTo = array();
+               }
+               // don't add the same address multiple times
+               foreach ($replyTo as $r) {
+                       if (isset($r['email']) && $r['email'] === $email) {
+                               return false;
+                       }
+               }
+               $replyTo[] = array('name' => $name, 'email' => $email);
+               return $this->setData('replyTo', $replyTo);
+       }
+       /**
         * Return a string containing the reply-to address.
         * @return string
        function getReplyToString($send = false) {
-               $replyTo = $this->getReplyTo();
-               if (!is_array($replyTo) || !array_key_exists('email', $replyTo) || $replyTo['email'] == null) {
-                       return null;
-               } else {
-                       return (Mail::encodeDisplayName($replyTo['name'], $send) . ' <'.$replyTo['email'].'>');
-               }
+               return $this->getAddressArrayString($this->getReplyTo(), true, $send);

@@ -498,6 +514,11 @@ class Mail extends DataObject {

                if (Config::getVar('email', 'force_default_envelope_sender') && Config::getVar('email', 'default_envelope_sender')) {
                        $this->addHeader('Return-Path', Config::getVar('email', 'default_envelope_sender'));
+                       $this->addHeader('Sender', Config::getVar('email', 'default_envelope_sender'));
+                       $fromAddress = $this->getFrom();
+                       if (is_array($fromAddress)) {
+                               $this->addReplyTo($fromAddress['email'], $fromAddress['name']);
+                       }

I haven’t deployed this against 3.x yet, but I think the biggest change would be here:

PHPMailer::SetFrom() sets the MAIL FROM (SMTP Sender), so this needs to occur before the $mailer->Sender = $s.

		if (($f = $this->getFrom()) != null) {
			$mailer->SetFrom($f['email'], $f['name']);
		if (($s = $this->getEnvelopeSender()) != null) $mailer->Sender = $s;

Optionally adding in a Repy-To header would be the final step:

		if (Config::getVar('email', 'force_default_envelope_sender') && Config::getVar('email', 'default_envelope_sender')) {
			if (($f = $this->getFrom()) != null) {
				$mailer->AddReplyTo($f['email'], $f['name']);

I’ve not tested any of the proposed 3.x code yet.

Thank you. You’ve done a much better job than I could of describing the problems and possible solutions.

I see now that Option to use a global sender address for emails (to fight spamfilters?) · Issue #4014 · pkp/pkp-lib · GitHub is pretty much this issue so I will follow up there.

So https://github.com/pkp/pkp-lib/issues/4014 was closed and applied but does not solve my problem with DMARC.

I have opened a new issue to specifically address DMARC here:

Let’s continue the technical aspects of this conversation in that new issue.

@ctgraham, I’m having problem getting my e-mails signed by OpenDKIM, I already set default_envelope_sender and forced it on all e-mails. The server admin said that the opendkim.conf directive was set, but I’m still not getting a signature. Will adding the reply-to patch to OJS solve this?

The only way to get a message properly DKIM-signed is to have both Sender and From equals.

OpenDKIM can be configured as to which header(s) it signs. For example, my configuration in /etc/opendkim.conf is:
SenderHeaders Sender,From

If the envelope sender is present, this address will be prioritized for signing. Otherwise, the from address is signed.

1 Like

What is the current status of this, and how can one workaround our e-mails being marked as potentially spam?
I am afraid this might be the reason I am getting a lot of missing reviews.

My current configuration is SPF record, default_envelope_sender set, and forcing the default envelope sender.
And running 3.1.1.

What can I do?
Thank you

If you also ensure that your mailserver is applying DKIM keys to outgoing mail, and is publishing a DMARC policy, you have the recommended configuration. Your hosting provider or system administrator can confirm or configure DKIM and DMARC.

An additional step could be to check your server status on known spam RBLs, such as https://www.spamhaus.org/ .

I am the sysadmin, so, it looks like I need to learn how to use DKIM.
Thank you!

If you are running your own postfix install, an easy solution is OpenDKIM at the server level:

Hopefully I have DKIM, SPF and DMARC installed, but gmail, when receiving emails, still complain that DMARC is not correct. Can I add details in this thread? Are you willing to help? :slight_smile:

This is a bit outside of the scope of our support for PKP products, but if you share the details here, I or someone else in the forum may see something which may help.

In OJS 3.1.2 you can set force_dmarc_compliant_from and force_dmarc compliant_from_displayname in config.inc.php.
This will force the envelope sender into the From: header as well, shifting the “real” From address into a Reply-To header.
The github issue linked earlier in this thread contains all the details of why this is needed.
If you must stay on OJS 3.1.1 then look for the comment from bdmckay that summarizes what patches to use.

It took some time to put the upgrade working, but it seems it solved the issue. Thank you, @tgc99.
Now I just need to complete PT translations.