Initial commit, DMARC checking support added
This commit is contained in:
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|

|
||||||
|
# VExim Plugin DNS Tools
|
||||||
|
This plugin is designed to work with the [VExim Web UI](https://github.com/MrSleeps/VExim-Web-UI).
|
||||||
|
|
||||||
|
It provides some useful dns tools, such as DKIM, DMARC and SPF checks.
|
||||||
|
|
||||||
|
You need to install [VExim Web UI](https://github.com/MrSleeps/VExim-Web-UI) for this to work
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "mrsleeps/vexim-web-plugin-dnstools",
|
||||||
|
"description": "DNS tools plugin for VExim Web UI dns plugins.",
|
||||||
|
"type": "filament-plugin",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"filament",
|
||||||
|
"dns",
|
||||||
|
"cloudflare",
|
||||||
|
"hosting"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/MrSleeps/vexim-web-plugin-dnstools",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "MrSleeps",
|
||||||
|
"homepage": "https://github.com/MrSleeps"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.4",
|
||||||
|
"filament/filament": "^5.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"VEximweb\\Plugin\\DnsTools\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"VEximweb\\Plugin\\DnsTools\\Providers\\DnsToolsServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^3.0",
|
||||||
|
"laravel/pint": "^1.0",
|
||||||
|
"nunomaduro/collision": "^8.0",
|
||||||
|
"orchestra/testbench": "^11.0",
|
||||||
|
"pestphp/pest": "^4.0",
|
||||||
|
"pestphp/pest-plugin-arch": "^4.0",
|
||||||
|
"pestphp/pest-plugin-laravel": "^4.0",
|
||||||
|
"pestphp/pest-plugin-livewire": "^4.0",
|
||||||
|
"rector/rector": "^2.0"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use VEximweb\Core\Data\Repositories\SettingRepository;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Using the setting repository to add DMARC settings
|
||||||
|
$repository = app(SettingRepository::class);
|
||||||
|
|
||||||
|
// DMARC core settings
|
||||||
|
$settings = [
|
||||||
|
'dmarc_policy' => [
|
||||||
|
'value' => 'none',
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'DMARC policy: none, quarantine, or reject',
|
||||||
|
],
|
||||||
|
'dmarc_subdomain_policy' => [
|
||||||
|
'value' => null,
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'DMARC subdomain policy: none, quarantine, or reject',
|
||||||
|
],
|
||||||
|
'dmarc_adkim' => [
|
||||||
|
'value' => 'relaxed',
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'DKIM alignment: relaxed or strict',
|
||||||
|
],
|
||||||
|
'dmarc_aspf' => [
|
||||||
|
'value' => 'relaxed',
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'SPF alignment: relaxed or strict',
|
||||||
|
],
|
||||||
|
'dmarc_percentage' => [
|
||||||
|
'value' => 100,
|
||||||
|
'type' => 'integer',
|
||||||
|
'description' => 'Percentage of messages to apply policy to (0-100)',
|
||||||
|
],
|
||||||
|
'dmarc_report_interval' => [
|
||||||
|
'value' => 86400,
|
||||||
|
'type' => 'integer',
|
||||||
|
'description' => 'Reporting interval in seconds (default 24 hours)',
|
||||||
|
],
|
||||||
|
'dmarc_t' => [
|
||||||
|
'value' => 'n',
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Testing mode: y (yes) or n (no)',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reporting addresses
|
||||||
|
'dmarc_rua' => [
|
||||||
|
'value' => [],
|
||||||
|
'type' => 'json',
|
||||||
|
'description' => 'Aggregate report email addresses',
|
||||||
|
],
|
||||||
|
'dmarc_ruf' => [
|
||||||
|
'value' => [],
|
||||||
|
'type' => 'json',
|
||||||
|
'description' => 'Forensic report email addresses',
|
||||||
|
],
|
||||||
|
'dmarc_reporting' => [
|
||||||
|
'value' => ['all'],
|
||||||
|
'type' => 'json',
|
||||||
|
'description' => 'Failure reporting options (fo)',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Advanced settings
|
||||||
|
'dmarc_np' => [
|
||||||
|
'value' => null,
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Non-existent subdomain policy',
|
||||||
|
],
|
||||||
|
'dmarc_psd' => [
|
||||||
|
'value' => null,
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Public suffix domain policy: y, n, or u',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Email localparts (overrides config values)
|
||||||
|
'dmarc_rua_localpart' => [
|
||||||
|
'value' => env('DMARC_RUA_LOCALPART', 'dmarc'),
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Custom local part for RUA reports',
|
||||||
|
],
|
||||||
|
'dmarc_ruf_localpart' => [
|
||||||
|
'value' => env('DMARC_RUF_LOCALPART', 'dmarc'),
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Custom local part for RUF reports',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($settings as $key => $config) {
|
||||||
|
// Check if setting already exists
|
||||||
|
if (!$repository->has($key)) {
|
||||||
|
$repository->set($key, $config['value'], $config['type'], $config['description']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$repository = app(SettingRepository::class);
|
||||||
|
|
||||||
|
$keys = [
|
||||||
|
'dmarc_policy',
|
||||||
|
'dmarc_subdomain_policy',
|
||||||
|
'dmarc_adkim',
|
||||||
|
'dmarc_aspf',
|
||||||
|
'dmarc_percentage',
|
||||||
|
'dmarc_report_interval',
|
||||||
|
'dmarc_t',
|
||||||
|
'dmarc_rua',
|
||||||
|
'dmarc_ruf',
|
||||||
|
'dmarc_reporting',
|
||||||
|
'dmarc_np',
|
||||||
|
'dmarc_psd',
|
||||||
|
'dmarc_rua_localpart',
|
||||||
|
'dmarc_ruf_localpart',
|
||||||
|
];
|
||||||
|
|
||||||
|
$repository->deleteMultiple($keys);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('vw_dmarc_checks', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('domain')->unique()->index();
|
||||||
|
$table->unsignedBigInteger('domain_id')->nullable()->index();
|
||||||
|
$table->text('record')->nullable();
|
||||||
|
$table->string('policy')->nullable()->index();
|
||||||
|
$table->string('subdomain_policy')->nullable();
|
||||||
|
$table->string('adkim')->nullable();
|
||||||
|
$table->string('aspf')->nullable();
|
||||||
|
$table->json('rua')->nullable();
|
||||||
|
$table->json('ruf')->nullable();
|
||||||
|
$table->json('reporting')->nullable();
|
||||||
|
$table->integer('percentage')->nullable();
|
||||||
|
$table->integer('report_interval')->nullable();
|
||||||
|
$table->string('np')->nullable();
|
||||||
|
$table->string('psd')->nullable();
|
||||||
|
$table->string('t')->nullable();
|
||||||
|
$table->boolean('valid')->default(false)->index();
|
||||||
|
$table->text('error_message')->nullable();
|
||||||
|
$table->timestamp('last_checked_at')->nullable()->index();
|
||||||
|
$table->timestamp('next_check_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Add foreign key constraint if needed (optional)
|
||||||
|
// $table->foreign('domain_id')->references('domain_id')->on('domains')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('vw_dmarc_checks');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<div class="space-y-6 text-foreground">
|
||||||
|
|
||||||
|
{{-- Header --}}
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-foreground">
|
||||||
|
{{ $domain }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
@if($dmarc && $dmarc->valid)
|
||||||
|
<span class="mt-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400">
|
||||||
|
Valid
|
||||||
|
</span>
|
||||||
|
@elseif($dmarc)
|
||||||
|
<span class="mt-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-500/10 text-red-600 dark:text-red-400">
|
||||||
|
Invalid
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="mt-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-muted text-muted-foreground">
|
||||||
|
Not Checked
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($dmarc && $dmarc->last_checked_at)
|
||||||
|
<div class="text-xs text-muted-foreground text-right">
|
||||||
|
Last checked<br>
|
||||||
|
<span class="text-foreground">
|
||||||
|
{{ $dmarc->last_checked_at->format('Y-m-d H:i:s') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($dmarc)
|
||||||
|
|
||||||
|
{{-- Error --}}
|
||||||
|
@if(!$dmarc->valid && $dmarc->error_message)
|
||||||
|
<div class="p-4 rounded-lg border border-red-500/20 bg-red-500/5">
|
||||||
|
<div class="text-sm font-medium text-red-500">
|
||||||
|
Error
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-sm text-red-400">
|
||||||
|
{{ $dmarc->error_message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Record --}}
|
||||||
|
@if($dmarc->record)
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">
|
||||||
|
DMARC Record
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 p-3 rounded-lg border border-border bg-card">
|
||||||
|
<code class="text-sm text-foreground break-all">
|
||||||
|
{{ $dmarc->record }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Grid --}}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">Policy</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground">
|
||||||
|
{{ ucfirst($dmarc->policy ?? 'Not set') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">Subdomain Policy</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground">
|
||||||
|
{{ $dmarc->subdomain_policy ? ucfirst($dmarc->subdomain_policy) : 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">DKIM Alignment</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground">
|
||||||
|
{{ $dmarc->adkim ? ucfirst($dmarc->adkim) : 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">SPF Alignment</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground">
|
||||||
|
{{ $dmarc->aspf ? ucfirst($dmarc->aspf) : 'Not set' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($dmarc->percentage !== null)
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">Percentage</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground">
|
||||||
|
{{ $dmarc->percentage }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- RUA / RUF --}}
|
||||||
|
@if($dmarc->rua || $dmarc->ruf)
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
|
@if($dmarc->rua)
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">Aggregate (RUA)</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground break-all">
|
||||||
|
{{ implode(', ', $dmarc->rua) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($dmarc->ruf)
|
||||||
|
<div class="p-3 rounded-lg border border-border bg-card">
|
||||||
|
<div class="text-xs text-muted-foreground uppercase">Forensic (RUF)</div>
|
||||||
|
<div class="mt-1 text-sm text-foreground break-all">
|
||||||
|
{{ implode(', ', $dmarc->ruf) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@else
|
||||||
|
|
||||||
|
<div class="text-center py-10 text-muted-foreground">
|
||||||
|
No DMARC record has been checked yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
Generate DMARC Record for {{ $record->domain }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Configure your DMARC policy below. The generated record will be saved and can be viewed in the DMARC management page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ $this->form }}
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
@if($dmarc && $dmarc->valid)
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">Domain</h4>
|
||||||
|
<p class="text-xl font-semibold mt-1">{{ $record->domain }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">Policy</h4>
|
||||||
|
<p class="text-xl font-semibold mt-1">
|
||||||
|
<span class="px-2 py-1 rounded text-sm font-medium bg-{{ $dmarc->getPolicyColor() }}-100 text-{{ $dmarc->getPolicyColor() }}-800">
|
||||||
|
{{ $dmarc->getPolicyLabel() }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500 ml-2">({{ $dmarc->policy }})</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Checked</h4>
|
||||||
|
<p class="text-xl font-semibold mt-1">{{ $dmarc->last_checked_at?->diffForHumans() ?? 'Never' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">DKIM Alignment</h4>
|
||||||
|
<p class="text-lg font-semibold mt-1">{{ ucfirst($dmarc->adkim ?? 'Not set') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">SPF Alignment</h4>
|
||||||
|
<p class="text-lg font-semibold mt-1">{{ ucfirst($dmarc->aspf ?? 'Not set') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">Enforcement Percentage</h4>
|
||||||
|
<p class="text-lg font-semibold mt-1">{{ $dmarc->percentage ?? 100 }}%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">Testing Mode</h4>
|
||||||
|
<p class="text-lg font-semibold mt-1">{{ $dmarc->t === 'y' ? '✅ On' : '❌ Off' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($dmarc->rua || $dmarc->ruf)
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-4">Reporting Addresses</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
@if($dmarc->rua)
|
||||||
|
<p class="text-sm"><span class="font-medium">RUA:</span> {{ implode(', ', $dmarc->rua) }}</p>
|
||||||
|
@endif
|
||||||
|
@if($dmarc->ruf)
|
||||||
|
<p class="text-sm"><span class="font-medium">RUF:</span> {{ implode(', ', $dmarc->ruf) }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-4">DNS Record</h4>
|
||||||
|
<div class="p-4 bg-gray-100 dark:bg-gray-900 rounded font-mono text-sm overflow-x-auto">
|
||||||
|
<code>v=DMARC1; {{ $dmarc->record }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-sm text-gray-500">
|
||||||
|
<p>Next check: {{ $dmarc->next_check_at?->diffForHumans() ?? 'Not scheduled' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-center py-12">
|
||||||
|
<div class="mb-4 text-4xl">📡</div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">No DMARC Record Found</h3>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Domain: <span class="font-mono">{{ $record->domain }}</span>
|
||||||
|
</p>
|
||||||
|
@if($dmarc?->error_message)
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $dmarc->error_message }}</p>
|
||||||
|
@endif
|
||||||
|
<div class="mt-6">
|
||||||
|
<a href="{{ \VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource::getUrl('generate', ['record' => $record]) }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg">
|
||||||
|
Generate DMARC Record
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
|
||||||
|
|
||||||
|
class CheckDmarcRecords extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'dmarc:check
|
||||||
|
{--domain= : Check a specific domain}
|
||||||
|
{--all : Check all domains}
|
||||||
|
{--stats : Show DMARC statistics}';
|
||||||
|
protected $description = 'Check DMARC records for domains';
|
||||||
|
|
||||||
|
public function handle(DmarcCheckService $service): void
|
||||||
|
{
|
||||||
|
if ($this->option('stats')) {
|
||||||
|
$stats = $service->getStats();
|
||||||
|
|
||||||
|
$this->info("DMARC Statistics:");
|
||||||
|
$this->line("Total domains checked: {$stats['total']}");
|
||||||
|
$this->line("Valid DMARC records: {$stats['valid']}");
|
||||||
|
$this->line("No DMARC record: {$stats['no_record']}");
|
||||||
|
$this->line("Invalid records: {$stats['invalid']}");
|
||||||
|
|
||||||
|
if (!empty($stats['policies'])) {
|
||||||
|
$this->line("\nPolicy Breakdown:");
|
||||||
|
foreach ($stats['policies'] as $policy => $count) {
|
||||||
|
$label = match($policy) {
|
||||||
|
'none' => 'Monitor',
|
||||||
|
'quarantine' => 'Quarantine',
|
||||||
|
'reject' => 'Reject',
|
||||||
|
default => $policy,
|
||||||
|
};
|
||||||
|
$this->line(" {$label}: {$count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('domain')) {
|
||||||
|
$domain = $this->option('domain');
|
||||||
|
$this->info("Checking DMARC for: {$domain}");
|
||||||
|
|
||||||
|
$result = $service->checkDomain($domain);
|
||||||
|
|
||||||
|
if ($result && $result->valid) {
|
||||||
|
$this->info("✓ DMARC record found!");
|
||||||
|
$this->line("Policy: {$result->getPolicyLabel()}");
|
||||||
|
$this->line("Record: {$result->record}");
|
||||||
|
} else {
|
||||||
|
$this->error("✗ No valid DMARC record found");
|
||||||
|
if ($result) {
|
||||||
|
$this->line("Error: {$result->error_message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('all')) {
|
||||||
|
$this->info('Checking DMARC for all domains...');
|
||||||
|
$service->checkAllDomains();
|
||||||
|
$this->info('Done!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: check domains that need updating
|
||||||
|
$this->info('Checking domains that need DMARC updates...');
|
||||||
|
$service->checkDomainsNeedingUpdate();
|
||||||
|
$this->info('Done!');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc;
|
||||||
|
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcPolicy;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcAlignment;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcReporting;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcTesting;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Exceptions\InvalidDmarcRecordException;
|
||||||
|
use Illuminate\Contracts\Support\Arrayable;
|
||||||
|
use Illuminate\Contracts\Support\Jsonable;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for building an object representation of a DMARC
|
||||||
|
* compliant string value
|
||||||
|
*
|
||||||
|
* @link https://mxtoolbox.com/dmarc/details/dmarc-tags
|
||||||
|
* @link https://datatracker.ietf.org/doc/html/rfc7489
|
||||||
|
*/
|
||||||
|
class DmarcRecord implements Arrayable, Jsonable, JsonSerializable
|
||||||
|
{
|
||||||
|
protected ?string $version = null;
|
||||||
|
protected ?DmarcPolicy $policy = null;
|
||||||
|
protected ?DmarcPolicy $subdomainPolicy = null;
|
||||||
|
protected ?string $rua = null;
|
||||||
|
protected ?string $ruf = null;
|
||||||
|
protected ?DmarcAlignment $adkim = null;
|
||||||
|
protected ?DmarcAlignment $aspf = null;
|
||||||
|
protected array $reporting = [];
|
||||||
|
protected ?int $percentage = null;
|
||||||
|
protected ?int $reportInterval = null;
|
||||||
|
protected ?DmarcPolicy $nonExistentSubdomainPolicy = null;
|
||||||
|
protected ?string $publicSuffixDomainPolicy = null;
|
||||||
|
protected ?DmarcTesting $testingMode = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DMARC record instance
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
?string $version = 'DMARC1',
|
||||||
|
DmarcPolicy|string|null $policy = DmarcPolicy::NONE,
|
||||||
|
DmarcPolicy|string|null $subdomainPolicy = null,
|
||||||
|
string|array|null $rua = null,
|
||||||
|
string|array|null $ruf = null,
|
||||||
|
DmarcAlignment|string|null $adkim = DmarcAlignment::RELAXED,
|
||||||
|
DmarcAlignment|string|null $aspf = DmarcAlignment::RELAXED,
|
||||||
|
array $reporting = [],
|
||||||
|
?int $percentage = null,
|
||||||
|
?int $reportInterval = null,
|
||||||
|
DmarcPolicy|string|null $nonExistentSubdomainPolicy = null,
|
||||||
|
?string $publicSuffixDomainPolicy = null,
|
||||||
|
DmarcTesting|string|null $testingMode = null
|
||||||
|
) {
|
||||||
|
$this->version = $version;
|
||||||
|
$this->setPolicy($policy);
|
||||||
|
$this->setSubdomainPolicy($subdomainPolicy);
|
||||||
|
$this->rua($rua);
|
||||||
|
$this->ruf($ruf);
|
||||||
|
$this->setAdkim($adkim);
|
||||||
|
$this->setAspf($aspf);
|
||||||
|
$this->reporting($reporting);
|
||||||
|
$this->percentage($percentage);
|
||||||
|
$this->reportInterval($reportInterval);
|
||||||
|
$this->setNonExistentSubdomainPolicy($nonExistentSubdomainPolicy);
|
||||||
|
$this->publicSuffixDomainPolicy($publicSuffixDomainPolicy);
|
||||||
|
$this->setTestingMode($testingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DMARC record from an array
|
||||||
|
*/
|
||||||
|
public static function fromArray(array $data): static
|
||||||
|
{
|
||||||
|
return new static(
|
||||||
|
version: Arr::get($data, 'version', 'DMARC1'),
|
||||||
|
policy: Arr::get($data, 'policy'),
|
||||||
|
subdomainPolicy: Arr::get($data, 'subdomain_policy'),
|
||||||
|
rua: Arr::get($data, 'rua'),
|
||||||
|
ruf: Arr::get($data, 'ruf'),
|
||||||
|
adkim: Arr::get($data, 'adkim'),
|
||||||
|
aspf: Arr::get($data, 'aspf'),
|
||||||
|
reporting: Arr::get($data, 'reporting', []),
|
||||||
|
percentage: Arr::get($data, 'percentage'),
|
||||||
|
reportInterval: Arr::get($data, 'report_interval'),
|
||||||
|
nonExistentSubdomainPolicy: Arr::get($data, 'np'),
|
||||||
|
publicSuffixDomainPolicy: Arr::get($data, 'psd'),
|
||||||
|
testingMode: Arr::get($data, 't'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create from a DNS record string
|
||||||
|
*/
|
||||||
|
public static function fromString(string $record): static
|
||||||
|
{
|
||||||
|
$builder = new static;
|
||||||
|
|
||||||
|
$properties = [];
|
||||||
|
|
||||||
|
// Parse the DMARC record
|
||||||
|
foreach (explode(';', $record) as $part) {
|
||||||
|
$property = explode('=', trim($part));
|
||||||
|
|
||||||
|
if (count($property) !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = trim($property[0]);
|
||||||
|
$value = trim($property[1]);
|
||||||
|
|
||||||
|
// Clean common issues:
|
||||||
|
// 1. Remove trailing periods (common in DNS zone files)
|
||||||
|
// 2. Remove surrounding quotes
|
||||||
|
// 3. Trim whitespace
|
||||||
|
$value = rtrim($value, '.');
|
||||||
|
$value = trim($value, '"\'');
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
$properties[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!isset($properties['v'])) {
|
||||||
|
throw InvalidDmarcRecordException::missingKey('v', 'DMARC version is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($properties['p'])) {
|
||||||
|
throw InvalidDmarcRecordException::missingKey('p', 'DMARC policy is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each property
|
||||||
|
foreach ($properties as $key => $value) {
|
||||||
|
match ($key) {
|
||||||
|
'v' => $builder->version($value),
|
||||||
|
'p' => $builder->policy($value),
|
||||||
|
'sp' => $builder->subdomainPolicy($value),
|
||||||
|
'rua' => $builder->rua($value),
|
||||||
|
'ruf' => $builder->ruf($value),
|
||||||
|
'adkim' => $builder->adkim($value),
|
||||||
|
'aspf' => $builder->aspf($value),
|
||||||
|
'fo' => $builder->reportingFromString($value),
|
||||||
|
'pct' => $builder->percentage((int) $value),
|
||||||
|
'ri' => $builder->reportInterval((int) $value),
|
||||||
|
'np' => $builder->nonExistentSubdomainPolicy($value),
|
||||||
|
'psd' => $builder->publicSuffixDomainPolicy($value),
|
||||||
|
't' => $builder->testingMode($value),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version
|
||||||
|
*/
|
||||||
|
public function version(?string $version): static
|
||||||
|
{
|
||||||
|
$this->version = $version;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set the policy
|
||||||
|
*/
|
||||||
|
public function policy(DmarcPolicy|string|null $policy): static
|
||||||
|
{
|
||||||
|
$this->setPolicy($policy);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set the subdomain policy
|
||||||
|
*/
|
||||||
|
public function subdomainPolicy(DmarcPolicy|string|null $policy): static
|
||||||
|
{
|
||||||
|
$this->setSubdomainPolicy($policy);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set RUA (aggregate report) URIs
|
||||||
|
*/
|
||||||
|
public function rua(string|array|null $mailto): static
|
||||||
|
{
|
||||||
|
$this->rua = $this->normalizeReportUris($mailto);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set RUF (forensic report) URIs
|
||||||
|
*/
|
||||||
|
public function ruf(string|array|null $mailto): static
|
||||||
|
{
|
||||||
|
$this->ruf = $this->normalizeReportUris($mailto);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize report URIs
|
||||||
|
*/
|
||||||
|
protected function normalizeReportUris(string|array|null $value): ?string
|
||||||
|
{
|
||||||
|
if (is_null($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = is_array($value) ? $value : explode(',', $value);
|
||||||
|
|
||||||
|
$items = array_values(array_filter(
|
||||||
|
array_map('trim', $items),
|
||||||
|
fn (string $item): bool => $item !== ''
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!str_starts_with($item, 'mailto:')) {
|
||||||
|
throw InvalidDmarcRecordException::invalidFormat(
|
||||||
|
'mailto address should start with "mailto:"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items === [] ? null : implode(',', $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set ADKIM (DKIM alignment)
|
||||||
|
*/
|
||||||
|
public function adkim(DmarcAlignment|string|null $value): static
|
||||||
|
{
|
||||||
|
$this->setAdkim($value);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set ASPF (SPF alignment)
|
||||||
|
*/
|
||||||
|
public function aspf(DmarcAlignment|string|null $value): static
|
||||||
|
{
|
||||||
|
$this->setAspf($value);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set reporting options (fo)
|
||||||
|
*/
|
||||||
|
public function reporting(array $values = []): static
|
||||||
|
{
|
||||||
|
$values = array_values(array_unique($values));
|
||||||
|
|
||||||
|
$processedValues = [];
|
||||||
|
|
||||||
|
foreach ($values as $value) {
|
||||||
|
if ($value instanceof DmarcReporting) {
|
||||||
|
$processedValues[] = $value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string and trim
|
||||||
|
$value = trim((string)$value);
|
||||||
|
|
||||||
|
// Check if it's a short code (0, 1, d, s)
|
||||||
|
if (in_array($value, ['0', '1', 'd', 's'], true)) {
|
||||||
|
try {
|
||||||
|
$processedValues[] = DmarcReporting::fromShortCode($value);
|
||||||
|
continue;
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
// Fall through to try from() method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create from the full value (all, any, dkim, spf)
|
||||||
|
try {
|
||||||
|
$processedValues[] = DmarcReporting::from($value);
|
||||||
|
} catch (\ValueError $e) {
|
||||||
|
throw InvalidDmarcRecordException::invalidValue(
|
||||||
|
"Invalid reporting option: {$value}. Must be 0, 1, d, s, all, any, dkim, or spf"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mutually exclusive options (0 and 1 cannot be combined)
|
||||||
|
$hasAll = in_array(DmarcReporting::ALL, $processedValues, true);
|
||||||
|
$hasAny = in_array(DmarcReporting::ANY, $processedValues, true);
|
||||||
|
|
||||||
|
if ($hasAll && $hasAny) {
|
||||||
|
throw InvalidDmarcRecordException::conflict(
|
||||||
|
'Reporting options "all" (0) and "any" (1) are mutually exclusive.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reporting = $processedValues;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse reporting options from string
|
||||||
|
*/
|
||||||
|
public function reportingFromString(string $value): static
|
||||||
|
{
|
||||||
|
// Remove whitespace and trim
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
// Clean the value - remove trailing periods and quotes
|
||||||
|
$value = rtrim($value, '.');
|
||||||
|
$value = trim($value, '"\'');
|
||||||
|
$value = trim($value);
|
||||||
|
|
||||||
|
// Check if it's a colon-separated list (e.g., "0:1:d:s")
|
||||||
|
if (str_contains($value, ':')) {
|
||||||
|
$options = array_map(function($item) {
|
||||||
|
$item = trim($item);
|
||||||
|
$item = rtrim($item, '.');
|
||||||
|
$item = trim($item, '"\'');
|
||||||
|
return trim($item);
|
||||||
|
}, explode(':', $value));
|
||||||
|
} else {
|
||||||
|
// Single value (e.g., "0", "1", "d", "s")
|
||||||
|
$options = [trim($value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out empty values
|
||||||
|
$options = array_filter($options, fn($v) => $v !== '');
|
||||||
|
|
||||||
|
// Convert numeric values to their string representations
|
||||||
|
$options = array_map(function($value) {
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return match((int)$value) {
|
||||||
|
0 => '0',
|
||||||
|
1 => '1',
|
||||||
|
default => (string)$value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}, $options);
|
||||||
|
|
||||||
|
$this->reporting($options);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set percentage (pct)
|
||||||
|
*/
|
||||||
|
public function percentage(?int $percentage): static
|
||||||
|
{
|
||||||
|
if (!is_null($percentage) && ($percentage < 0 || $percentage > 100)) {
|
||||||
|
throw InvalidDmarcRecordException::invalidValue(
|
||||||
|
'Percentage must be between 0 and 100'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->percentage = $percentage;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set reporting interval (ri)
|
||||||
|
*/
|
||||||
|
public function reportInterval(?int $interval): static
|
||||||
|
{
|
||||||
|
if (!is_null($interval) && $interval < 0) {
|
||||||
|
throw InvalidDmarcRecordException::invalidValue(
|
||||||
|
'Reporting interval must be a positive integer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->reportInterval = $interval;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set non-existent subdomain policy (np)
|
||||||
|
*/
|
||||||
|
public function nonExistentSubdomainPolicy(DmarcPolicy|string|null $policy): static
|
||||||
|
{
|
||||||
|
$this->setNonExistentSubdomainPolicy($policy);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set public suffix domain policy (psd)
|
||||||
|
*/
|
||||||
|
public function publicSuffixDomainPolicy(?string $policy): static
|
||||||
|
{
|
||||||
|
if (!is_null($policy) && !in_array($policy, ['y', 'n', 'u', null], true)) {
|
||||||
|
throw InvalidDmarcRecordException::invalidValue(
|
||||||
|
'PSD must be "y", "n", or "u"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->publicSuffixDomainPolicy = $policy;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get/set testing mode (t)
|
||||||
|
*/
|
||||||
|
public function testingMode(DmarcTesting|string|null $testingMode): static
|
||||||
|
{
|
||||||
|
$this->setTestingMode($testingMode);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to a DNS record string
|
||||||
|
*/
|
||||||
|
public function toDnsRecord(): string
|
||||||
|
{
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
if ($this->version) {
|
||||||
|
$parts[] = "v={$this->version}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->policy) {
|
||||||
|
$parts[] = "p={$this->policy->value}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->subdomainPolicy) {
|
||||||
|
$parts[] = "sp={$this->subdomainPolicy->value}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->rua) {
|
||||||
|
$parts[] = "rua={$this->rua}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->ruf) {
|
||||||
|
$parts[] = "ruf={$this->ruf}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->adkim) {
|
||||||
|
$parts[] = "adkim={$this->adkim->getShortCode()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->aspf) {
|
||||||
|
$parts[] = "aspf={$this->aspf->getShortCode()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->reporting)) {
|
||||||
|
$foParts = array_map(
|
||||||
|
fn(DmarcReporting $option) => $option->getShortCode(),
|
||||||
|
$this->reporting
|
||||||
|
);
|
||||||
|
$parts[] = "fo=" . implode(':', $foParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_null($this->percentage)) {
|
||||||
|
$parts[] = "pct={$this->percentage}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_null($this->reportInterval)) {
|
||||||
|
$parts[] = "ri={$this->reportInterval}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($this->nonExistentSubdomainPolicy) {
|
||||||
|
$parts[] = "np={$this->nonExistentSubdomainPolicy->value}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->publicSuffixDomainPolicy) {
|
||||||
|
$parts[] = "psd={$this->publicSuffixDomainPolicy}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->testingMode) {
|
||||||
|
$parts[] = "t={$this->testingMode->value}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('; ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all validation rules for Filament forms
|
||||||
|
*/
|
||||||
|
public static function getValidationRules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'policy' => ['required', 'string', 'in:none,quarantine,reject'],
|
||||||
|
'subdomain_policy' => ['nullable', 'string', 'in:none,quarantine,reject'],
|
||||||
|
'rua' => ['nullable', 'array'],
|
||||||
|
'rua.*' => ['string', 'starts_with:mailto:'],
|
||||||
|
'ruf' => ['nullable', 'array'],
|
||||||
|
'ruf.*' => ['string', 'starts_with:mailto:'],
|
||||||
|
'adkim' => ['nullable', 'string', 'in:relaxed,strict'],
|
||||||
|
'aspf' => ['nullable', 'string', 'in:relaxed,strict'],
|
||||||
|
'reporting' => ['nullable', 'array'],
|
||||||
|
'reporting.*' => ['string', 'in:all,any,dkim,spf'],
|
||||||
|
'percentage' => ['nullable', 'integer', 'min:0', 'max:100'],
|
||||||
|
'report_interval' => ['nullable', 'integer', 'min:0'],
|
||||||
|
'np' => ['nullable', 'string', 'in:none,quarantine,reject'],
|
||||||
|
'psd' => ['nullable', 'string', 'in:y,n,u'],
|
||||||
|
't' => ['nullable', 'string', 'in:y,n'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to array
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'version' => $this->version,
|
||||||
|
'policy' => $this->policy?->value,
|
||||||
|
'subdomain_policy' => $this->subdomainPolicy?->value,
|
||||||
|
'rua' => $this->rua ? explode(',', $this->rua) : null,
|
||||||
|
'ruf' => $this->ruf ? explode(',', $this->ruf) : null,
|
||||||
|
'adkim' => $this->adkim?->value,
|
||||||
|
'aspf' => $this->aspf?->value,
|
||||||
|
'reporting' => array_map(fn($opt) => $opt->value, $this->reporting),
|
||||||
|
'percentage' => $this->percentage,
|
||||||
|
'report_interval' => $this->reportInterval,
|
||||||
|
'np' => $this->nonExistentSubdomainPolicy?->value,
|
||||||
|
'psd' => $this->publicSuffixDomainPolicy,
|
||||||
|
't' => $this->testingMode?->value,
|
||||||
|
'dns_record' => $this->toDnsRecord(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to JSON
|
||||||
|
*/
|
||||||
|
public function toJson($options = 0): string
|
||||||
|
{
|
||||||
|
return json_encode($this->toArray(), $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON serialize
|
||||||
|
*/
|
||||||
|
public function jsonSerialize(): mixed
|
||||||
|
{
|
||||||
|
return $this->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magic method for string conversion
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->toDnsRecord();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private setter methods with validation
|
||||||
|
|
||||||
|
private function setPolicy(DmarcPolicy|string|null $policy): void
|
||||||
|
{
|
||||||
|
if (is_null($policy)) {
|
||||||
|
$this->policy = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->policy = $policy instanceof DmarcPolicy
|
||||||
|
? $policy
|
||||||
|
: DmarcPolicy::from($policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSubdomainPolicy(DmarcPolicy|string|null $policy): void
|
||||||
|
{
|
||||||
|
if (is_null($policy)) {
|
||||||
|
$this->subdomainPolicy = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->subdomainPolicy = $policy instanceof DmarcPolicy
|
||||||
|
? $policy
|
||||||
|
: DmarcPolicy::from($policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setAdkim(DmarcAlignment|string|null $value): void
|
||||||
|
{
|
||||||
|
if (is_null($value)) {
|
||||||
|
$this->adkim = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DmarcAlignment) {
|
||||||
|
$this->adkim = $value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a short code (r or s)
|
||||||
|
if (in_array($value, ['r', 's'], true)) {
|
||||||
|
$this->adkim = DmarcAlignment::fromShortCode($value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create from the string value directly
|
||||||
|
try {
|
||||||
|
$this->adkim = DmarcAlignment::from($value);
|
||||||
|
} catch (\ValueError $e) {
|
||||||
|
throw InvalidDmarcRecordException::invalidValue(
|
||||||
|
"Invalid ADKIM value: '{$value}'. Must be 'relaxed', 'strict', 'r', or 's'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setAspf(DmarcAlignment|string|null $value): void
|
||||||
|
{
|
||||||
|
if (is_null($value)) {
|
||||||
|
$this->aspf = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DmarcAlignment) {
|
||||||
|
$this->aspf = $value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a short code (r or s)
|
||||||
|
if (in_array($value, ['r', 's'], true)) {
|
||||||
|
$this->aspf = DmarcAlignment::fromShortCode($value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create from the string value directly
|
||||||
|
try {
|
||||||
|
$this->aspf = DmarcAlignment::from($value);
|
||||||
|
} catch (\ValueError $e) {
|
||||||
|
throw InvalidDmarcRecordException::invalidValue(
|
||||||
|
"Invalid ASPF value: '{$value}'. Must be 'relaxed', 'strict', 'r', or 's'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setNonExistentSubdomainPolicy(DmarcPolicy|string|null $policy): void
|
||||||
|
{
|
||||||
|
if (is_null($policy)) {
|
||||||
|
$this->nonExistentSubdomainPolicy = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->nonExistentSubdomainPolicy = $policy instanceof DmarcPolicy
|
||||||
|
? $policy
|
||||||
|
: DmarcPolicy::from($policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setTestingMode(DmarcTesting|string|null $testingMode): void
|
||||||
|
{
|
||||||
|
if (is_null($testingMode)) {
|
||||||
|
$this->testingMode = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->testingMode = $testingMode instanceof DmarcTesting
|
||||||
|
? $testingMode
|
||||||
|
: DmarcTesting::from($testingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(): ?string
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy(): ?DmarcPolicy
|
||||||
|
{
|
||||||
|
return $this->policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubdomainPolicy(): ?DmarcPolicy
|
||||||
|
{
|
||||||
|
return $this->subdomainPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRua(): ?string
|
||||||
|
{
|
||||||
|
return $this->rua;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRuf(): ?string
|
||||||
|
{
|
||||||
|
return $this->ruf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAdkim(): ?DmarcAlignment
|
||||||
|
{
|
||||||
|
return $this->adkim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAspf(): ?DmarcAlignment
|
||||||
|
{
|
||||||
|
return $this->aspf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReporting(): array
|
||||||
|
{
|
||||||
|
return $this->reporting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPercentage(): ?int
|
||||||
|
{
|
||||||
|
return $this->percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReportInterval(): ?int
|
||||||
|
{
|
||||||
|
return $this->reportInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNonExistentSubdomainPolicy(): ?DmarcPolicy
|
||||||
|
{
|
||||||
|
return $this->nonExistentSubdomainPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPublicSuffixDomainPolicy(): ?string
|
||||||
|
{
|
||||||
|
return $this->publicSuffixDomainPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTestingMode(): ?DmarcTesting
|
||||||
|
{
|
||||||
|
return $this->testingMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc\Enums;
|
||||||
|
|
||||||
|
enum DmarcAlignment: string
|
||||||
|
{
|
||||||
|
case RELAXED = 'relaxed';
|
||||||
|
case STRICT = 'strict';
|
||||||
|
|
||||||
|
public function getShortCode(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::RELAXED => 'r',
|
||||||
|
self::STRICT => 's',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::RELAXED => 'Relaxed alignment (subdomains allowed)',
|
||||||
|
self::STRICT => 'Strict alignment (exact match required)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromShortCode(string $code): self
|
||||||
|
{
|
||||||
|
return match($code) {
|
||||||
|
'r' => self::RELAXED,
|
||||||
|
's' => self::STRICT,
|
||||||
|
default => throw new \InvalidArgumentException("Invalid alignment code: {$code}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc\Enums;
|
||||||
|
|
||||||
|
enum DmarcPolicy: string
|
||||||
|
{
|
||||||
|
case NONE = 'none';
|
||||||
|
case QUARANTINE = 'quarantine';
|
||||||
|
case REJECT = 'reject';
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::NONE => 'No action, monitor only',
|
||||||
|
self::QUARANTINE => 'Mark as spam/quarantine',
|
||||||
|
self::REJECT => 'Reject the email outright',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSeverity(): int
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::NONE => 0,
|
||||||
|
self::QUARANTINE => 1,
|
||||||
|
self::REJECT => 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc\Enums;
|
||||||
|
|
||||||
|
enum DmarcReporting: string
|
||||||
|
{
|
||||||
|
case ALL = 'all';
|
||||||
|
case ANY = 'any';
|
||||||
|
case DKIM = 'dkim';
|
||||||
|
case SPF = 'spf';
|
||||||
|
|
||||||
|
public function getShortCode(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::ALL => '0',
|
||||||
|
self::ANY => '1',
|
||||||
|
self::DKIM => 'd',
|
||||||
|
self::SPF => 's',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::ALL => 'Report all failures',
|
||||||
|
self::ANY => 'Report if either DKIM or SPF fails',
|
||||||
|
self::DKIM => 'Report only DKIM failures',
|
||||||
|
self::SPF => 'Report only SPF failures',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromShortCode(string $code): self
|
||||||
|
{
|
||||||
|
return match($code) {
|
||||||
|
'0' => self::ALL,
|
||||||
|
'1' => self::ANY,
|
||||||
|
'd' => self::DKIM,
|
||||||
|
's' => self::SPF,
|
||||||
|
default => throw new \InvalidArgumentException("Invalid reporting option code: {$code}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc\Enums;
|
||||||
|
|
||||||
|
enum DmarcTesting: string
|
||||||
|
{
|
||||||
|
case YES = 'y';
|
||||||
|
case NO = 'n';
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::YES => 'Testing mode (don\'t apply policy)',
|
||||||
|
self::NO => 'Normal mode (apply policy)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class InvalidDmarcRecordException extends Exception
|
||||||
|
{
|
||||||
|
public static function invalidValue(string $message): self
|
||||||
|
{
|
||||||
|
return new self("Invalid DMARC value: {$message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function missingKey(string $key, string $message): self
|
||||||
|
{
|
||||||
|
return new self("Missing required key '{$key}': {$message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidFormat(string $message): self
|
||||||
|
{
|
||||||
|
return new self("Invalid DMARC format: {$message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function conflict(string $message): self
|
||||||
|
{
|
||||||
|
return new self("DMARC configuration conflict: {$message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Dmarc\Services;
|
||||||
|
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\DmarcRecord;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Exceptions\InvalidDmarcRecordException;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DmarcCheckService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check a single domain's DMARC record
|
||||||
|
*/
|
||||||
|
public function checkDomain(string $domain, ?int $domainId = null): ?DmarcCheck
|
||||||
|
{
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$attempt = 1;
|
||||||
|
$maxAttempts = 3;
|
||||||
|
$lastError = null;
|
||||||
|
|
||||||
|
Log::info('DMARC check started', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'domain_id' => $domainId,
|
||||||
|
'timestamp' => now()->toDateTimeString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
while ($attempt <= $maxAttempts) {
|
||||||
|
$attemptStart = microtime(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::debug('DMARC check attempt', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'max_attempts' => $maxAttempts
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add delay between attempts (except first)
|
||||||
|
if ($attempt > 1) {
|
||||||
|
$delay = 1;
|
||||||
|
Log::debug('Adding delay before retry', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'delay_seconds' => $delay
|
||||||
|
]);
|
||||||
|
sleep($delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase DNS timeout for remote domains
|
||||||
|
$originalTimeout = ini_get('dns.timeout');
|
||||||
|
$originalRetry = ini_get('dns.retry');
|
||||||
|
|
||||||
|
Log::debug('DNS settings before change', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'original_timeout' => $originalTimeout,
|
||||||
|
'original_retry' => $originalRetry
|
||||||
|
]);
|
||||||
|
|
||||||
|
ini_set('dns.timeout', '10');
|
||||||
|
ini_set('dns.retry', '5');
|
||||||
|
|
||||||
|
Log::debug('DNS settings after change', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'new_timeout' => ini_get('dns.timeout'),
|
||||||
|
'new_retry' => ini_get('dns.retry')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Query DNS for DMARC record
|
||||||
|
$dnsStart = microtime(true);
|
||||||
|
$records = @dns_get_record("_dmarc.{$domain}", DNS_TXT);
|
||||||
|
$dnsDuration = microtime(true) - $dnsStart;
|
||||||
|
|
||||||
|
// Restore original settings
|
||||||
|
if ($originalTimeout !== false) {
|
||||||
|
ini_set('dns.timeout', $originalTimeout);
|
||||||
|
}
|
||||||
|
if ($originalRetry !== false) {
|
||||||
|
ini_set('dns.retry', $originalRetry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::debug('DNS query completed', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'duration_ms' => round($dnsDuration * 1000, 2),
|
||||||
|
'success' => $records !== false,
|
||||||
|
'record_count' => $records ? count($records) : 0,
|
||||||
|
'records_found' => $records ? array_column($records, 'txt') : []
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If DNS query failed or returned no records
|
||||||
|
if ($records === false) {
|
||||||
|
$lastError = 'DNS query returned false (error)';
|
||||||
|
Log::warning('DNS query returned false', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'duration_ms' => round($dnsDuration * 1000, 2)
|
||||||
|
]);
|
||||||
|
$attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($records)) {
|
||||||
|
$lastError = 'No DMARC record found (empty response)';
|
||||||
|
Log::warning('DNS query returned empty', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'duration_ms' => round($dnsDuration * 1000, 2)
|
||||||
|
]);
|
||||||
|
$attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the DMARC record (starts with v=DMARC1)
|
||||||
|
$dmarcRecord = null;
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$txt = $record['txt'] ?? '';
|
||||||
|
if (str_starts_with(trim($txt), 'v=DMARC1')) {
|
||||||
|
$dmarcRecord = $txt;
|
||||||
|
Log::debug('Found DMARC record', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'record' => $dmarcRecord
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dmarcRecord) {
|
||||||
|
$lastError = 'No valid DMARC record found (v=DMARC1 missing)';
|
||||||
|
Log::warning('No DMARC record with v=DMARC1 found', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'records' => array_column($records, 'txt')
|
||||||
|
]);
|
||||||
|
$attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the DMARC record
|
||||||
|
Log::debug('Parsing DMARC record', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'record' => $dmarcRecord
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$parsed = DmarcRecord::fromString($dmarcRecord);
|
||||||
|
} catch (InvalidDmarcRecordException $e) {
|
||||||
|
$lastError = 'Invalid DMARC record: ' . $e->getMessage();
|
||||||
|
Log::warning('Invalid DMARC record', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'record' => $dmarcRecord
|
||||||
|
]);
|
||||||
|
$attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to cache
|
||||||
|
$totalDuration = microtime(true) - $startTime;
|
||||||
|
|
||||||
|
Log::info('DMARC check successful', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'total_duration_ms' => round($totalDuration * 1000, 2),
|
||||||
|
'dns_duration_ms' => round($dnsDuration * 1000, 2),
|
||||||
|
'policy' => $parsed->getPolicy()?->value,
|
||||||
|
'record' => $dmarcRecord
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->saveSuccessfulCheck($domain, $domainId, $dmarcRecord, $parsed);
|
||||||
|
|
||||||
|
} catch (InvalidDmarcRecordException $e) {
|
||||||
|
$lastError = 'Invalid DMARC record: ' . $e->getMessage();
|
||||||
|
Log::warning('DMARC check attempt failed (InvalidDmarcRecordException)', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
$attempt++;
|
||||||
|
continue;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$lastError = 'Error checking DMARC: ' . $e->getMessage();
|
||||||
|
Log::warning('DMARC check attempt failed (Exception)', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
$attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All attempts failed
|
||||||
|
$totalDuration = microtime(true) - $startTime;
|
||||||
|
|
||||||
|
Log::error('DMARC check failed after all attempts', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'attempts' => $attempt - 1,
|
||||||
|
'total_duration_ms' => round($totalDuration * 1000, 2),
|
||||||
|
'last_error' => $lastError,
|
||||||
|
'timestamp' => now()->toDateTimeString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->saveFailedCheck($domain, $domainId, $lastError ?? 'Unknown error after ' . ($attempt - 1) . ' attempts');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check all domains from the domains table
|
||||||
|
*/
|
||||||
|
public function checkAllDomains(): void
|
||||||
|
{
|
||||||
|
Log::info('Starting DMARC check for all domains');
|
||||||
|
|
||||||
|
// Get domains from the domains table
|
||||||
|
$domains = \VEximweb\Core\Data\Models\Domain::where('enabled', true)
|
||||||
|
->select('domain_id', 'domain')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
Log::info('Found domains to check', [
|
||||||
|
'total' => $domains->count()
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
Log::debug('Checking domain', [
|
||||||
|
'domain' => $domain->domain,
|
||||||
|
'domain_id' => $domain->domain_id
|
||||||
|
]);
|
||||||
|
$this->checkDomain($domain->domain, $domain->domain_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Finished DMARC check for all domains');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check domains that need checking (next_check_at <= now)
|
||||||
|
*/
|
||||||
|
public function checkDomainsNeedingUpdate(): void
|
||||||
|
{
|
||||||
|
Log::info('Starting DMARC check for domains needing update');
|
||||||
|
|
||||||
|
// First, get all domains from the domains table
|
||||||
|
$domains = \VEximweb\Core\Data\Models\Domain::where('enabled', true)
|
||||||
|
->select('domain_id', 'domain')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$checked = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
// Check if this domain needs updating
|
||||||
|
$cache = DmarcCheck::where('domain', $domain->domain)->first();
|
||||||
|
|
||||||
|
if (!$cache || ($cache->next_check_at && $cache->next_check_at <= now())) {
|
||||||
|
Log::debug('Domain needs update', [
|
||||||
|
'domain' => $domain->domain,
|
||||||
|
'last_check' => $cache?->last_checked_at,
|
||||||
|
'next_check' => $cache?->next_check_at
|
||||||
|
]);
|
||||||
|
$this->checkDomain($domain->domain, $domain->domain_id);
|
||||||
|
$checked++;
|
||||||
|
} else {
|
||||||
|
Log::debug('Domain skipped (not due for update)', [
|
||||||
|
'domain' => $domain->domain,
|
||||||
|
'next_check' => $cache->next_check_at
|
||||||
|
]);
|
||||||
|
$skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Finished DMARC check for domains needing update', [
|
||||||
|
'checked' => $checked,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'total' => $domains->count()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a successful check
|
||||||
|
*/
|
||||||
|
protected function saveSuccessfulCheck(string $domain, ?int $domainId, string $record, DmarcRecord $parsed): DmarcCheck
|
||||||
|
{
|
||||||
|
Log::debug('Saving successful check', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'policy' => $parsed->getPolicy()?->value
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = DmarcCheck::updateOrCreate(
|
||||||
|
['domain' => $domain],
|
||||||
|
[
|
||||||
|
'domain_id' => $domainId,
|
||||||
|
'record' => $record,
|
||||||
|
'policy' => $parsed->getPolicy()?->value,
|
||||||
|
'subdomain_policy' => $parsed->getSubdomainPolicy()?->value,
|
||||||
|
'adkim' => $parsed->getAdkim()?->value,
|
||||||
|
'aspf' => $parsed->getAspf()?->value,
|
||||||
|
'rua' => $parsed->getRua() ? explode(',', $parsed->getRua()) : null,
|
||||||
|
'ruf' => $parsed->getRuf() ? explode(',', $parsed->getRuf()) : null,
|
||||||
|
'reporting' => $parsed->getReporting() ? array_map(fn($r) => $r->value, $parsed->getReporting()) : null,
|
||||||
|
'percentage' => $parsed->getPercentage(),
|
||||||
|
'report_interval' => $parsed->getReportInterval(),
|
||||||
|
'np' => $parsed->getNonExistentSubdomainPolicy()?->value,
|
||||||
|
'psd' => $parsed->getPublicSuffixDomainPolicy(),
|
||||||
|
't' => $parsed->getTestingMode()?->value,
|
||||||
|
'valid' => true,
|
||||||
|
'error_message' => null,
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'next_check_at' => now()->addHours(24),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::debug('Saved successful check', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'id' => $result->id,
|
||||||
|
'valid' => $result->valid
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a failed check
|
||||||
|
*/
|
||||||
|
protected function saveFailedCheck(string $domain, ?int $domainId, string $error): DmarcCheck
|
||||||
|
{
|
||||||
|
Log::debug('Saving failed check', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'error' => $error
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = DmarcCheck::updateOrCreate(
|
||||||
|
['domain' => $domain],
|
||||||
|
[
|
||||||
|
'domain_id' => $domainId,
|
||||||
|
'valid' => false,
|
||||||
|
'error_message' => $error,
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'next_check_at' => now()->addHours(6),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::debug('Saved failed check', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'id' => $result->id,
|
||||||
|
'valid' => $result->valid,
|
||||||
|
'error' => $result->error_message
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache for a domain
|
||||||
|
*/
|
||||||
|
public function clearCache(string $domain): void
|
||||||
|
{
|
||||||
|
Log::info('Clearing DMARC cache', ['domain' => $domain]);
|
||||||
|
DmarcCheck::where('domain', $domain)->delete();
|
||||||
|
Log::debug('DMARC cache cleared', ['domain' => $domain]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached DMARC record for a domain
|
||||||
|
*/
|
||||||
|
public function getCached(string $domain): ?DmarcCheck
|
||||||
|
{
|
||||||
|
Log::debug('Getting cached DMARC record', ['domain' => $domain]);
|
||||||
|
|
||||||
|
$cache = DmarcCheck::where('domain', $domain)->first();
|
||||||
|
|
||||||
|
if (!$cache) {
|
||||||
|
Log::debug('No cached record found, checking domain', ['domain' => $domain]);
|
||||||
|
return $this->checkDomain($domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cache->next_check_at && $cache->next_check_at <= now()) {
|
||||||
|
Log::debug('Cached record expired, rechecking', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'next_check' => $cache->next_check_at,
|
||||||
|
'now' => now()
|
||||||
|
]);
|
||||||
|
return $this->checkDomain($domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::debug('Returning cached record', [
|
||||||
|
'domain' => $domain,
|
||||||
|
'valid' => $cache->valid,
|
||||||
|
'policy' => $cache->policy
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about DMARC records
|
||||||
|
*/
|
||||||
|
public function getStats(): array
|
||||||
|
{
|
||||||
|
Log::debug('Getting DMARC statistics');
|
||||||
|
|
||||||
|
$total = DmarcCheck::count();
|
||||||
|
$valid = DmarcCheck::where('valid', true)->count();
|
||||||
|
$invalid = DmarcCheck::where('valid', false)->count();
|
||||||
|
|
||||||
|
$policies = DmarcCheck::where('valid', true)
|
||||||
|
->select('policy', \DB::raw('count(*) as count'))
|
||||||
|
->groupBy('policy')
|
||||||
|
->pluck('count', 'policy')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$noRecord = DmarcCheck::where('valid', false)
|
||||||
|
->where('error_message', 'No DMARC record found')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total' => $total,
|
||||||
|
'valid' => $valid,
|
||||||
|
'invalid' => $invalid,
|
||||||
|
'policies' => $policies,
|
||||||
|
'no_record' => $noRecord,
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::debug('DMARC statistics', $stats);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource\Pages;
|
||||||
|
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Slider;
|
||||||
|
use Filament\Forms\Components\Placeholder;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\DmarcRecord;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcPolicy;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcAlignment;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcReporting;
|
||||||
|
use VEximweb\Core\Data\Models\Domain;
|
||||||
|
|
||||||
|
class GenerateDmarc extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = DmarcResource::class;
|
||||||
|
|
||||||
|
public Domain $domain;
|
||||||
|
public ?DmarcCheck $existingDmarc = null;
|
||||||
|
|
||||||
|
public function mount($record = null): void
|
||||||
|
{
|
||||||
|
if ($record) {
|
||||||
|
$this->domain = Domain::find($record);
|
||||||
|
$this->existingDmarc = DmarcCheck::where('domain', $this->domain->domain)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::mount();
|
||||||
|
|
||||||
|
if ($this->existingDmarc) {
|
||||||
|
$defaults = [
|
||||||
|
'policy' => $this->existingDmarc->policy ?? 'none',
|
||||||
|
'subdomain_policy' => $this->existingDmarc->subdomain_policy ?? null,
|
||||||
|
'adkim' => $this->existingDmarc->adkim ?? 'relaxed',
|
||||||
|
'aspf' => $this->existingDmarc->aspf ?? 'relaxed',
|
||||||
|
'reporting' => json_decode($this->existingDmarc->reporting ?? '["all"]', true),
|
||||||
|
'percentage' => $this->existingDmarc->percentage ?? 100,
|
||||||
|
't' => $this->existingDmarc->t ?? 'n',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->existingDmarc->rua) {
|
||||||
|
$rua = json_decode($this->existingDmarc->rua, true);
|
||||||
|
if (is_array($rua)) {
|
||||||
|
$defaults['rua'] = collect($rua)->map(function ($item) {
|
||||||
|
return ['email' => str_replace('mailto:', '', $item)];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->existingDmarc->ruf) {
|
||||||
|
$ruf = json_decode($this->existingDmarc->ruf, true);
|
||||||
|
if (is_array($ruf)) {
|
||||||
|
$defaults['ruf'] = collect($ruf)->map(function ($item) {
|
||||||
|
return ['email' => str_replace('mailto:', '', $item)];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->form->fill($defaults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
$service = app(DmarcCheckService::class);
|
||||||
|
$addresses = $service->getDefaultReportingAddresses($this->domain->domain ?? config('app.domain'));
|
||||||
|
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Select::make('policy')
|
||||||
|
->label('Policy')
|
||||||
|
->options([
|
||||||
|
'none' => 'None (Monitor only)',
|
||||||
|
'quarantine' => 'Quarantine (Mark as spam)',
|
||||||
|
'reject' => 'Reject (Block delivery)',
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->default('none')
|
||||||
|
->live(),
|
||||||
|
|
||||||
|
Select::make('subdomain_policy')
|
||||||
|
->label('Subdomain Policy')
|
||||||
|
->options([
|
||||||
|
'' => 'Inherit from main policy',
|
||||||
|
'none' => 'None (Monitor only)',
|
||||||
|
'quarantine' => 'Quarantine (Mark as spam)',
|
||||||
|
'reject' => 'Reject (Block delivery)',
|
||||||
|
])
|
||||||
|
->nullable(),
|
||||||
|
|
||||||
|
Repeater::make('rua')
|
||||||
|
->label('Aggregate Reports (RUA)')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('email')
|
||||||
|
->label('Email')
|
||||||
|
->email()
|
||||||
|
->prefix('mailto:')
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->default(function () use ($addresses) {
|
||||||
|
if ($this->existingDmarc && $this->existingDmarc->rua) {
|
||||||
|
$rua = json_decode($this->existingDmarc->rua, true);
|
||||||
|
if (is_array($rua)) {
|
||||||
|
return collect($rua)->map(function ($item) {
|
||||||
|
return ['email' => str_replace('mailto:', '', $item)];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [['email' => str_replace('mailto:', '', $addresses['rua'])]];
|
||||||
|
})
|
||||||
|
->addable()
|
||||||
|
->deletable()
|
||||||
|
->reorderable()
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Repeater::make('ruf')
|
||||||
|
->label('Forensic Reports (RUF)')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('email')
|
||||||
|
->label('Email')
|
||||||
|
->email()
|
||||||
|
->prefix('mailto:')
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->default(function () use ($addresses) {
|
||||||
|
if ($this->existingDmarc && $this->existingDmarc->ruf) {
|
||||||
|
$ruf = json_decode($this->existingDmarc->ruf, true);
|
||||||
|
if (is_array($ruf)) {
|
||||||
|
return collect($ruf)->map(function ($item) {
|
||||||
|
return ['email' => str_replace('mailto:', '', $item)];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [['email' => str_replace('mailto:', '', $addresses['ruf'])]];
|
||||||
|
})
|
||||||
|
->addable()
|
||||||
|
->deletable()
|
||||||
|
->reorderable()
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Select::make('adkim')
|
||||||
|
->label('DKIM Alignment')
|
||||||
|
->options([
|
||||||
|
'relaxed' => 'Relaxed (subdomains allowed)',
|
||||||
|
'strict' => 'Strict (exact match)',
|
||||||
|
])
|
||||||
|
->default('relaxed'),
|
||||||
|
|
||||||
|
Select::make('aspf')
|
||||||
|
->label('SPF Alignment')
|
||||||
|
->options([
|
||||||
|
'relaxed' => 'Relaxed (subdomains allowed)',
|
||||||
|
'strict' => 'Strict (exact match)',
|
||||||
|
])
|
||||||
|
->default('relaxed'),
|
||||||
|
|
||||||
|
Select::make('reporting')
|
||||||
|
->label('Failure Reporting (FO)')
|
||||||
|
->multiple()
|
||||||
|
->options([
|
||||||
|
'all' => 'All failures',
|
||||||
|
'any' => 'Any failure',
|
||||||
|
'dkim' => 'DKIM failures only',
|
||||||
|
'spf' => 'SPF failures only',
|
||||||
|
])
|
||||||
|
->default(['all']),
|
||||||
|
|
||||||
|
Slider::make('percentage')
|
||||||
|
->label('Enforcement Percentage')
|
||||||
|
->min(0)
|
||||||
|
->max(100)
|
||||||
|
->default(100),
|
||||||
|
|
||||||
|
Select::make('t')
|
||||||
|
->label('Testing Mode')
|
||||||
|
->options([
|
||||||
|
'n' => 'Off (Apply policy)',
|
||||||
|
'y' => 'On (Don\'t apply policy)',
|
||||||
|
])
|
||||||
|
->default('n'),
|
||||||
|
|
||||||
|
Placeholder::make('generated_record_preview')
|
||||||
|
->label('Preview')
|
||||||
|
->content(function ($get) {
|
||||||
|
try {
|
||||||
|
$settings = $this->prepareSettings($get);
|
||||||
|
$dmarcRecord = new DmarcRecord(
|
||||||
|
policy: DmarcPolicy::tryFrom($settings['policy']),
|
||||||
|
subdomainPolicy: $settings['subdomain_policy'] ? DmarcPolicy::tryFrom($settings['subdomain_policy']) : null,
|
||||||
|
rua: $settings['rua'],
|
||||||
|
ruf: $settings['ruf'],
|
||||||
|
adkim: DmarcAlignment::tryFrom($settings['adkim']),
|
||||||
|
aspf: DmarcAlignment::tryFrom($settings['aspf']),
|
||||||
|
reporting: array_map(fn($r) => DmarcReporting::tryFrom($r), $settings['reporting']),
|
||||||
|
percentage: (int)$settings['percentage'],
|
||||||
|
t: $settings['t'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->domain) {
|
||||||
|
return "Name: _dmarc.{$this->domain->domain}\n" .
|
||||||
|
"Type: TXT\n" .
|
||||||
|
"Value: v=DMARC1; " . $dmarcRecord->toDnsRecord();
|
||||||
|
}
|
||||||
|
return "Please select a domain to preview";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return "Error generating preview: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->extraAttributes(['class' => 'font-mono text-sm bg-gray-50 dark:bg-gray-800 p-4 rounded'])
|
||||||
|
->columnSpanFull(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareSettings(array $data): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'policy' => $data['policy'] ?? 'none',
|
||||||
|
'subdomain_policy' => $data['subdomain_policy'] ?? null,
|
||||||
|
'rua' => collect($data['rua'] ?? [])
|
||||||
|
->filter(fn($item) => !empty($item['email']))
|
||||||
|
->map(fn($item) => 'mailto:' . $item['email'])
|
||||||
|
->values()
|
||||||
|
->toArray(),
|
||||||
|
'ruf' => collect($data['ruf'] ?? [])
|
||||||
|
->filter(fn($item) => !empty($item['email']))
|
||||||
|
->map(fn($item) => 'mailto:' . $item['email'])
|
||||||
|
->values()
|
||||||
|
->toArray(),
|
||||||
|
'adkim' => $data['adkim'] ?? 'relaxed',
|
||||||
|
'aspf' => $data['aspf'] ?? 'relaxed',
|
||||||
|
'reporting' => $data['reporting'] ?? ['all'],
|
||||||
|
'percentage' => (int)($data['percentage'] ?? 100),
|
||||||
|
't' => $data['t'] ?? 'n',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handleRecordCreation(array $data): \Illuminate\Database\Eloquent\Model
|
||||||
|
{
|
||||||
|
$settings = $this->prepareSettings($data);
|
||||||
|
|
||||||
|
$dmarcRecord = new DmarcRecord(
|
||||||
|
policy: DmarcPolicy::tryFrom($settings['policy']),
|
||||||
|
subdomainPolicy: $settings['subdomain_policy'] ? DmarcPolicy::tryFrom($settings['subdomain_policy']) : null,
|
||||||
|
rua: $settings['rua'],
|
||||||
|
ruf: $settings['ruf'],
|
||||||
|
adkim: DmarcAlignment::tryFrom($settings['adkim']),
|
||||||
|
aspf: DmarcAlignment::tryFrom($settings['aspf']),
|
||||||
|
reporting: array_map(fn($r) => DmarcReporting::tryFrom($r), $settings['reporting']),
|
||||||
|
percentage: (int)$settings['percentage'],
|
||||||
|
t: $settings['t'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$dnsRecord = 'v=DMARC1; ' . $dmarcRecord->toDnsRecord();
|
||||||
|
|
||||||
|
return DmarcCheck::updateOrCreate(
|
||||||
|
['domain' => $this->domain->domain],
|
||||||
|
[
|
||||||
|
'domain_id' => $this->domain->domain_id,
|
||||||
|
'record' => $dnsRecord,
|
||||||
|
'policy' => $settings['policy'],
|
||||||
|
'subdomain_policy' => $settings['subdomain_policy'],
|
||||||
|
'adkim' => $settings['adkim'],
|
||||||
|
'aspf' => $settings['aspf'],
|
||||||
|
'rua' => json_encode($settings['rua']),
|
||||||
|
'ruf' => json_encode($settings['ruf']),
|
||||||
|
'reporting' => json_encode($settings['reporting']),
|
||||||
|
'percentage' => (int)$settings['percentage'],
|
||||||
|
't' => $settings['t'],
|
||||||
|
'valid' => true,
|
||||||
|
'error_message' => null,
|
||||||
|
'last_checked_at' => now(),
|
||||||
|
'next_check_at' => now()->addHours(24),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCreatedNotificationTitle(): ?string
|
||||||
|
{
|
||||||
|
return "DMARC record generated for {$this->domain->domain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRedirectUrl(): string
|
||||||
|
{
|
||||||
|
return DmarcResource::getUrl('view', ['record' => $this->domain->getKey()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('back')
|
||||||
|
->label('Back to View')
|
||||||
|
->icon('heroicon-o-arrow-left')
|
||||||
|
->url(DmarcResource::getUrl('view', ['record' => $this->domain->getKey()])),
|
||||||
|
|
||||||
|
Action::make('back_to_list')
|
||||||
|
->label('Back to List')
|
||||||
|
->icon('heroicon-o-list-bullet')
|
||||||
|
->url(DmarcResource::getUrl('index')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
$action = $this->existingDmarc ? 'Update' : 'Generate';
|
||||||
|
return "{$action} DMARC Record";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc;
|
||||||
|
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Columns\BadgeColumn;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
|
||||||
|
use VEximweb\Core\Data\Models\Domain;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class ListDmarc extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = DmarcResource::class;
|
||||||
|
|
||||||
|
protected function getTableQuery(): Builder
|
||||||
|
{
|
||||||
|
return Domain::where('enabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->query($this->getTableQuery())
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('domain')
|
||||||
|
->label('Domain')
|
||||||
|
->searchable()
|
||||||
|
->sortable()
|
||||||
|
->size('lg')
|
||||||
|
->weight('bold'),
|
||||||
|
|
||||||
|
BadgeColumn::make('dmarc_status')
|
||||||
|
->label('DMARC Status')
|
||||||
|
->getStateUsing(function ($record) {
|
||||||
|
$dmarc = DmarcCheck::where('domain', $record->domain)->first();
|
||||||
|
if (!$dmarc) {
|
||||||
|
return 'Not Checked';
|
||||||
|
}
|
||||||
|
if (!$dmarc->valid) {
|
||||||
|
return 'No Record';
|
||||||
|
}
|
||||||
|
return $dmarc->getPolicyLabel();
|
||||||
|
})
|
||||||
|
->colors([
|
||||||
|
'success' => 'Monitor',
|
||||||
|
'warning' => 'Quarantine',
|
||||||
|
'danger' => 'Reject',
|
||||||
|
'gray' => ['Not Checked', 'No Record'],
|
||||||
|
]),
|
||||||
|
|
||||||
|
BadgeColumn::make('dmarc_policy')
|
||||||
|
->label('Policy')
|
||||||
|
->getStateUsing(function ($record) {
|
||||||
|
$dmarc = DmarcCheck::where('domain', $record->domain)->first();
|
||||||
|
return $dmarc?->policy ?? '-';
|
||||||
|
})
|
||||||
|
->colors([
|
||||||
|
'success' => 'none',
|
||||||
|
'warning' => 'quarantine',
|
||||||
|
'danger' => 'reject',
|
||||||
|
'gray' => '-',
|
||||||
|
]),
|
||||||
|
|
||||||
|
TextColumn::make('dmarc_last_checked')
|
||||||
|
->label('Last Checked')
|
||||||
|
->getStateUsing(function ($record) {
|
||||||
|
$dmarc = DmarcCheck::where('domain', $record->domain)->first();
|
||||||
|
return $dmarc?->last_checked_at;
|
||||||
|
})
|
||||||
|
->since()
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('exim_users_count')
|
||||||
|
->label('Accounts')
|
||||||
|
->counts('eximUsers')
|
||||||
|
->badge()
|
||||||
|
->color('info'),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
\Filament\Tables\Filters\SelectFilter::make('dmarc_status')
|
||||||
|
->label('DMARC Status')
|
||||||
|
->options([
|
||||||
|
'has_record' => 'Has DMARC Record',
|
||||||
|
'no_record' => 'No DMARC Record',
|
||||||
|
'not_checked' => 'Not Checked',
|
||||||
|
])
|
||||||
|
->query(function ($query, $data) {
|
||||||
|
if (empty($data['value'])) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match($data['value']) {
|
||||||
|
'has_record' => $query->whereIn('domain',
|
||||||
|
DmarcCheck::where('valid', true)->pluck('domain')->toArray()
|
||||||
|
),
|
||||||
|
'no_record' => $query->whereIn('domain',
|
||||||
|
DmarcCheck::where('valid', false)->pluck('domain')->toArray()
|
||||||
|
),
|
||||||
|
'not_checked' => $query->whereNotIn('domain',
|
||||||
|
DmarcCheck::pluck('domain')->toArray()
|
||||||
|
),
|
||||||
|
default => $query,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
\Filament\Tables\Filters\SelectFilter::make('policy')
|
||||||
|
->label('Policy')
|
||||||
|
->options([
|
||||||
|
'none' => 'Monitor',
|
||||||
|
'quarantine' => 'Quarantine',
|
||||||
|
'reject' => 'Reject',
|
||||||
|
])
|
||||||
|
->query(function ($query, $data) {
|
||||||
|
if (empty($data['value'])) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
$domains = DmarcCheck::where('policy', $data['value'])
|
||||||
|
->where('valid', true)
|
||||||
|
->pluck('domain')
|
||||||
|
->toArray();
|
||||||
|
return $query->whereIn('domain', $domains);
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Action::make('view')
|
||||||
|
->label('View DMARC')
|
||||||
|
->icon('heroicon-o-eye')
|
||||||
|
->color('info')
|
||||||
|
->url(fn ($record): string => DmarcResource::getUrl('view', ['record' => $record])),
|
||||||
|
|
||||||
|
Action::make('generate')
|
||||||
|
->label('Generate Record')
|
||||||
|
->icon('heroicon-o-document-plus')
|
||||||
|
->color('success')
|
||||||
|
->url(fn ($record): string => DmarcResource::getUrl('generate', ['record' => $record])),
|
||||||
|
|
||||||
|
Action::make('check_dns')
|
||||||
|
->label('Check DNS')
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->color('warning')
|
||||||
|
->action(function ($record) {
|
||||||
|
$service = app(DmarcCheckService::class);
|
||||||
|
$result = $service->checkDomain($record->domain, $record->domain_id);
|
||||||
|
|
||||||
|
if ($result && $result->valid) {
|
||||||
|
Notification::make()
|
||||||
|
->success()
|
||||||
|
->title('DMARC Record Found')
|
||||||
|
->body("Policy: {$result->getPolicyLabel()}")
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->warning()
|
||||||
|
->title('No DMARC Record Found')
|
||||||
|
->body($result?->error_message ?? 'No DMARC record found in DNS')
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
Action::make('check_all_dmarc')
|
||||||
|
->label('Check DMARC for Selected')
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->action(function ($records) {
|
||||||
|
$service = app(DmarcCheckService::class);
|
||||||
|
$valid = 0;
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$result = $service->checkDomain($record->domain, $record->domain_id);
|
||||||
|
if ($result && $result->valid) {
|
||||||
|
$valid++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->success()
|
||||||
|
->title('DMARC Check Complete')
|
||||||
|
->body("Checked {$records->count()} domains, {$valid} have valid DMARC records")
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc;
|
||||||
|
|
||||||
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
|
||||||
|
|
||||||
|
class ViewDmarc extends ViewRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = DmarcResource::class;
|
||||||
|
|
||||||
|
public ?DmarcCheck $dmarc = null;
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeFill(array $data): array
|
||||||
|
{
|
||||||
|
// Get the DMARC record for this domain
|
||||||
|
$this->dmarc = DmarcCheck::where('domain', $this->record->domain)->first();
|
||||||
|
|
||||||
|
// If no cache or invalid, check now
|
||||||
|
if (!$this->dmarc || !$this->dmarc->valid) {
|
||||||
|
$service = app(DmarcCheckService::class);
|
||||||
|
$this->dmarc = $service->checkDomain($this->record->domain, $this->record->domain_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('check_dns')
|
||||||
|
->label('Check DNS Again')
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->color('warning')
|
||||||
|
->action(function () {
|
||||||
|
$service = app(DmarcCheckService::class);
|
||||||
|
$this->dmarc = $service->checkDomain($this->record->domain, $this->record->domain_id);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->success()
|
||||||
|
->title('DMARC Record Updated')
|
||||||
|
->body($this->dmarc?->valid ? 'Record found and updated' : 'No valid record found')
|
||||||
|
->send();
|
||||||
|
|
||||||
|
$this->refreshFormData();
|
||||||
|
}),
|
||||||
|
|
||||||
|
Action::make('generate')
|
||||||
|
->label('Generate New Record')
|
||||||
|
->icon('heroicon-o-document-plus')
|
||||||
|
->color('success')
|
||||||
|
->url(DmarcResource::getUrl('generate', ['record' => $this->record])),
|
||||||
|
|
||||||
|
Action::make('back')
|
||||||
|
->label('Back to List')
|
||||||
|
->icon('heroicon-o-arrow-left')
|
||||||
|
->url(DmarcResource::getUrl('index')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getViewData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dmarc' => $this->dmarc,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "DMARC Record for {$this->record->domain}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Providers;
|
||||||
|
|
||||||
|
use Filament\Panel;
|
||||||
|
use Filament\PanelProvider;
|
||||||
|
use Filament\Support\Colors\Color;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\DmarcResource;
|
||||||
|
|
||||||
|
class DnsToolsPanelProvider extends PanelProvider
|
||||||
|
{
|
||||||
|
public function panel(Panel $panel): Panel
|
||||||
|
{
|
||||||
|
return $panel
|
||||||
|
->id('dns-tools')
|
||||||
|
->path('dns-tools')
|
||||||
|
->colors([
|
||||||
|
'primary' => Color::Blue,
|
||||||
|
])
|
||||||
|
->resources([
|
||||||
|
DmarcResource::class,
|
||||||
|
])
|
||||||
|
->navigationGroups([
|
||||||
|
'DNS Tools',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Resources;
|
||||||
|
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use VEximweb\Core\Data\Models\Domain;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\DmarcRecord;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcPolicy;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcAlignment;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Enums\DmarcReporting;
|
||||||
|
use BackedEnum;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc\ListDmarc;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc\ViewDmarc;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Pages\Dmarc\GenerateDmarc;
|
||||||
|
|
||||||
|
class DmarcResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Domain::class;
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||||
|
protected static string|\UnitEnum|null $navigationGroup = 'DNS Tools';
|
||||||
|
protected static ?string $navigationLabel = 'DMARC Management';
|
||||||
|
protected static ?string $pluralLabel = 'DMARC Management';
|
||||||
|
protected static ?int $navigationSort = 1;
|
||||||
|
protected static ?string $slug = 'dnstools/dmarc';
|
||||||
|
|
||||||
|
public static function getModel(): string
|
||||||
|
{
|
||||||
|
return Domain::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListDmarc::route('/'),
|
||||||
|
'view' => ViewDmarc::route('/{record}/view'),
|
||||||
|
'generate' => GenerateDmarc::route('/{record}/generate'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Pages;
|
||||||
|
|
||||||
|
use VEximweb\Core\Domain\Filament\Resources\DomainResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListDomains extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = DomainResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Resources;
|
||||||
|
|
||||||
|
//use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Pages\CreateDomain;
|
||||||
|
//use VEximweb\Core\Domain\Filament\Resources\Pages\EditDomain;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\Pages\ListDomains;
|
||||||
|
use VEximweb\Core\Domain\Filament\Resources\Schemas\DomainForm;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\Tables\DomainsTable;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\SystemDomains as Domain;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class DnsToolsResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Domain::class;
|
||||||
|
|
||||||
|
protected static ?string $slug = 'dnstools/domains';
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'domain';
|
||||||
|
|
||||||
|
protected static string|\UnitEnum|null $navigationGroup = 'DNS Tools';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'DNS Tools';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
// Get the base form
|
||||||
|
$schema = DomainForm::configure($schema);
|
||||||
|
|
||||||
|
// Check if DNS Core is installed and try to get extensions
|
||||||
|
if (class_exists(\VEximweb\Plugin\DnsCore\Services\DnsProviderDiscoveryService::class)) {
|
||||||
|
try {
|
||||||
|
$discoveryService = app(\VEximweb\Plugin\DnsCore\Services\DnsProviderDiscoveryService::class);
|
||||||
|
|
||||||
|
// Make sure the discovery service is booted
|
||||||
|
if (method_exists($discoveryService, 'boot')) {
|
||||||
|
$discoveryService->boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
$extensions = $discoveryService->getFormExtensions();
|
||||||
|
|
||||||
|
if (!empty($extensions)) {
|
||||||
|
$extensionComponents = [];
|
||||||
|
|
||||||
|
foreach ($extensions as $extension) {
|
||||||
|
if (isset($extension['components']) && is_callable($extension['components'])) {
|
||||||
|
$components = $extension['components']();
|
||||||
|
if (is_array($components)) {
|
||||||
|
$extensionComponents = array_merge($extensionComponents, $components);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($extensionComponents)) {
|
||||||
|
\Log::debug('Injecting DNS form extensions', [
|
||||||
|
'component_count' => count($extensionComponents),
|
||||||
|
'extension_count' => count($extensions),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schema = $schema->components([
|
||||||
|
...$schema->getComponents(),
|
||||||
|
...$extensionComponents,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::debug('Could not load DNS form extensions: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form_old(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return DomainForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return DomainsTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getEloquentQuery(): Builder
|
||||||
|
{
|
||||||
|
$query = parent::getEloquentQuery();
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
// If no user is logged in, return empty query
|
||||||
|
if (!$user) {
|
||||||
|
return $query->whereRaw('1 = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// System admins can see all domains
|
||||||
|
if ($user->isSystemAdmin()) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain admins only see domains they're assigned to
|
||||||
|
if ($user->isDomainAdmin()) {
|
||||||
|
return $query->whereHas('administrators', function ($q) use ($user) {
|
||||||
|
$q->where('user_id', $user->id)
|
||||||
|
->where('role', 'domain_admin');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain users shouldn't see any domains
|
||||||
|
return $query->whereRaw('1 = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static function getNavigationBadgeColor(): ?string
|
||||||
|
{
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationBadgeTooltip(): ?string
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->isSystemAdmin()) {
|
||||||
|
return 'Total number of domains in the system';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->isDomainAdmin()) {
|
||||||
|
return 'Total number of domains you administer';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide navigation item for domain-users
|
||||||
|
public static function shouldRegisterNavigation(): bool
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
// Hide for domain-user
|
||||||
|
if (!$user || $user->isDomainUser()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListDomains::route('/'),
|
||||||
|
//'create' => CreateDomain::route('/create'),
|
||||||
|
//'edit' => EditDomain::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getGloballySearchableAttributes(): array
|
||||||
|
{
|
||||||
|
return ['domain'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Pages;
|
||||||
|
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListDomains extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = DnsToolsResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Filament\Resources\Tables;
|
||||||
|
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\SystemDomains;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use VEximweb\Core\EximUser\Filament\Resources\EximUserResource;
|
||||||
|
use VEximweb\Core\EximAlias\Filament\Resources\EximAliasResource;
|
||||||
|
use VEximweb\Core\EximCatchAll\Filament\Resources\EximCatchAllResource;
|
||||||
|
use VEximweb\Core\EximFail\Filament\Resources\EximFailResource;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Actions\ActionGroup;
|
||||||
|
|
||||||
|
class DomainsTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordUrl(null)
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('domain')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
IconColumn::make('enabled')
|
||||||
|
->boolean()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
IconColumn::make('dkim.enabled')
|
||||||
|
->label('DKIM')
|
||||||
|
->boolean()
|
||||||
|
->trueIcon('heroicon-o-key')
|
||||||
|
->falseIcon('heroicon-o-x-circle')
|
||||||
|
->trueColor('success')
|
||||||
|
->falseColor('gray')
|
||||||
|
->tooltip(function ($record): string {
|
||||||
|
if ($record->dkim && $record->dkim->enabled) {
|
||||||
|
return "DKIM active (selector: {$record->dkim->selector})";
|
||||||
|
} elseif ($record->dkim && !$record->dkim->enabled) {
|
||||||
|
return "DKIM keys exist but are disabled";
|
||||||
|
} else {
|
||||||
|
return "No DKIM keys configured";
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
IconColumn::make('dmarccheck.valid')
|
||||||
|
->label('DMARC')
|
||||||
|
->boolean()
|
||||||
|
->trueIcon('heroicon-o-shield-check')
|
||||||
|
->falseIcon('heroicon-o-exclamation-triangle')
|
||||||
|
->trueColor('success')
|
||||||
|
->falseColor('danger')
|
||||||
|
->tooltip(function ($record): string {
|
||||||
|
$dmarc = $record->dmarcCheck;
|
||||||
|
|
||||||
|
if (!$dmarc) {
|
||||||
|
return 'DMARC not checked yet';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dmarc->valid) {
|
||||||
|
return "DMARC valid (Policy: {$dmarc->getPolicyLabel()})";
|
||||||
|
} else {
|
||||||
|
return "DMARC invalid: {$dmarc->error_message}";
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
SelectFilter::make('enabled')
|
||||||
|
->options([
|
||||||
|
'1' => 'Enabled',
|
||||||
|
'0' => 'Disabled',
|
||||||
|
]),
|
||||||
|
|
||||||
|
SelectFilter::make('dkim_status')
|
||||||
|
->label('DKIM Status')
|
||||||
|
->options([
|
||||||
|
'has_dkim' => 'Has DKIM (Active)',
|
||||||
|
'has_disabled' => 'Has DKIM (Disabled)',
|
||||||
|
'no_dkim' => 'No DKIM',
|
||||||
|
])
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
if ($data['value'] === 'has_dkim') {
|
||||||
|
return $query->whereHas('dkim', function ($q) {
|
||||||
|
$q->where('enabled', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($data['value'] === 'has_disabled') {
|
||||||
|
return $query->whereHas('dkim', function ($q) {
|
||||||
|
$q->where('enabled', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($data['value'] === 'no_dkim') {
|
||||||
|
return $query->whereDoesntHave('dkim');
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// DMARC Status Filter
|
||||||
|
SelectFilter::make('dmarc_status')
|
||||||
|
->label('DMARC Status')
|
||||||
|
->options([
|
||||||
|
'valid' => 'Valid DMARC',
|
||||||
|
'invalid' => 'Invalid DMARC',
|
||||||
|
'not_checked' => 'Not Checked',
|
||||||
|
])
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
if ($data['value'] === 'valid') {
|
||||||
|
return $query->whereHas('dmarcCheck', function ($q) {
|
||||||
|
$q->where('valid', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($data['value'] === 'invalid') {
|
||||||
|
return $query->whereHas('dmarcCheck', function ($q) {
|
||||||
|
$q->where('valid', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($data['value'] === 'not_checked') {
|
||||||
|
return $query->whereDoesntHave('dmarcCheck');
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// DMARC Policy Filter
|
||||||
|
SelectFilter::make('dmarc_policy')
|
||||||
|
->label('DMARC Policy')
|
||||||
|
->options([
|
||||||
|
'none' => 'Monitor (none)',
|
||||||
|
'quarantine' => 'Quarantine',
|
||||||
|
'reject' => 'Reject',
|
||||||
|
])
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
if ($data['value']) {
|
||||||
|
return $query->whereHas('dmarcCheck', function ($q) use ($data) {
|
||||||
|
$q->where('policy', $data['value'])
|
||||||
|
->where('valid', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
->recordActions([
|
||||||
|
Action::make('checkDmarc')
|
||||||
|
->icon(Heroicon::ArrowPath)
|
||||||
|
->label('')
|
||||||
|
->tooltip('Check DMARC record now')
|
||||||
|
->color('info')
|
||||||
|
->action(function ($record) {
|
||||||
|
try {
|
||||||
|
$service = app(\VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService::class);
|
||||||
|
$result = $service->checkDomain($record->domain, $record->domain_id);
|
||||||
|
|
||||||
|
if ($result && $result->valid) {
|
||||||
|
Notification::make()
|
||||||
|
->title('DMARC check completed')
|
||||||
|
->body("Valid DMARC record found for {$record->domain}")
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
$error = $result ? $result->error_message : 'Unknown error';
|
||||||
|
Notification::make()
|
||||||
|
->title('DMARC check completed')
|
||||||
|
->body("No valid DMARC record for {$record->domain}: {$error}")
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Notification::make()
|
||||||
|
->title('DMARC check failed')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
//EditAction::make(),
|
||||||
|
ActionGroup::make([
|
||||||
|
EditAction::make(),
|
||||||
|
EditAction::make(),
|
||||||
|
EditAction::make(),
|
||||||
|
Action::make('viewDmarc')
|
||||||
|
->label('DMARC Details')
|
||||||
|
->modalHeading('DMARC Record Details')
|
||||||
|
->modalSubheading(fn ($record) => $record->domain)
|
||||||
|
|
||||||
|
->modalContent(fn ($record) => view(
|
||||||
|
'dns-tools::filament.modals.dmarc-details',
|
||||||
|
[
|
||||||
|
'domain' => $record->domain,
|
||||||
|
'dmarc' => $record->dmarcCheck,
|
||||||
|
'record' => $record,
|
||||||
|
]
|
||||||
|
))
|
||||||
|
|
||||||
|
->modalWidth('3xl')
|
||||||
|
|
||||||
|
->modalActions([
|
||||||
|
Action::make('checkAgain')
|
||||||
|
->label('Check Again')
|
||||||
|
->action(function ($record) {
|
||||||
|
$service = app(\VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService::class);
|
||||||
|
$service->checkDomain($record->domain, $record->domain_id);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('DMARC check completed')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
|
|
||||||
|
Action::make('close')
|
||||||
|
->label('Close')
|
||||||
|
->close(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
// Add bulk action to check DMARC for selected domains
|
||||||
|
Action::make('checkSelectedDmarc')
|
||||||
|
->label('Check DMARC for selected')
|
||||||
|
->icon(Heroicon::ArrowPath)
|
||||||
|
->color('info')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->action(function ($records) {
|
||||||
|
$service = app(\VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService::class);
|
||||||
|
$count = 0;
|
||||||
|
$valid = 0;
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$result = $service->checkDomain($record->domain, $record->domain_id);
|
||||||
|
$count++;
|
||||||
|
if ($result && $result->valid) {
|
||||||
|
$valid++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title("DMARC check completed for {$count} domains")
|
||||||
|
->body("{$valid} domains have valid DMARC records")
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached DMARC records for domains
|
||||||
|
*/
|
||||||
|
class DmarcCheck extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'vw_dmarc_checks';
|
||||||
|
protected $primaryKey = 'id';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'domain',
|
||||||
|
'domain_id', // Foreign key to domains.domain_id (just for reference)
|
||||||
|
'record',
|
||||||
|
'policy',
|
||||||
|
'subdomain_policy',
|
||||||
|
'adkim',
|
||||||
|
'aspf',
|
||||||
|
'rua',
|
||||||
|
'ruf',
|
||||||
|
'reporting',
|
||||||
|
'percentage',
|
||||||
|
'report_interval',
|
||||||
|
'np',
|
||||||
|
'psd',
|
||||||
|
't',
|
||||||
|
'valid',
|
||||||
|
'error_message',
|
||||||
|
'last_checked_at',
|
||||||
|
'next_check_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'valid' => 'boolean',
|
||||||
|
'last_checked_at' => 'datetime',
|
||||||
|
'next_check_at' => 'datetime',
|
||||||
|
'percentage' => 'integer',
|
||||||
|
'report_interval' => 'integer',
|
||||||
|
'rua' => 'array',
|
||||||
|
'ruf' => 'array',
|
||||||
|
'reporting' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the domain this DMARC check belongs to (optional relation)
|
||||||
|
*/
|
||||||
|
public function domain(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\VEximweb\Core\Data\Models\Domain::class, 'domain_id', 'domain_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this domain has a DMARC record
|
||||||
|
*/
|
||||||
|
public function hasRecord(): bool
|
||||||
|
{
|
||||||
|
return $this->valid && !empty($this->record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the DMARC record as a string
|
||||||
|
*/
|
||||||
|
public function getRecordString(): ?string
|
||||||
|
{
|
||||||
|
return $this->record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the policy with a nice label
|
||||||
|
*/
|
||||||
|
public function getPolicyLabel(): string
|
||||||
|
{
|
||||||
|
$labels = [
|
||||||
|
'none' => 'Monitor',
|
||||||
|
'quarantine' => 'Quarantine',
|
||||||
|
'reject' => 'Reject',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $labels[$this->policy] ?? 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the policy with a color
|
||||||
|
*/
|
||||||
|
public function getPolicyColor(): string
|
||||||
|
{
|
||||||
|
return match($this->policy) {
|
||||||
|
'none' => 'success',
|
||||||
|
'quarantine' => 'warning',
|
||||||
|
'reject' => 'danger',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get policy icon
|
||||||
|
*/
|
||||||
|
public function getPolicyIcon(): string
|
||||||
|
{
|
||||||
|
return match($this->policy) {
|
||||||
|
'none' => 'heroicon-o-eye',
|
||||||
|
'quarantine' => 'heroicon-o-shield-exclamation',
|
||||||
|
'reject' => 'heroicon-o-x-circle',
|
||||||
|
default => 'heroicon-o-question-mark-circle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Models;
|
||||||
|
|
||||||
|
use VEximweb\Core\Data\Models\Domain as BaseDomain;
|
||||||
|
use VEximweb\Plugin\DnsTools\Models\DmarcCheck;
|
||||||
|
|
||||||
|
class SystemDomains extends BaseDomain
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the DMARC check for this domain
|
||||||
|
*/
|
||||||
|
public function dmarcCheck()
|
||||||
|
{
|
||||||
|
return $this->hasOne(DmarcCheck::class, 'domain_id', 'domain_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if domain has a valid DMARC record
|
||||||
|
*/
|
||||||
|
public function hasValidDmarc(): bool
|
||||||
|
{
|
||||||
|
$dmarc = $this->dmarcCheck()->first();
|
||||||
|
return $dmarc && $dmarc->valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DMARC status
|
||||||
|
*/
|
||||||
|
public function getDmarcStatus(): string
|
||||||
|
{
|
||||||
|
$dmarc = $this->dmarcCheck()->first();
|
||||||
|
if (!$dmarc) {
|
||||||
|
return 'not_checked';
|
||||||
|
}
|
||||||
|
return $dmarc->valid ? 'valid' : 'invalid';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Providers;
|
||||||
|
|
||||||
|
use Filament\Contracts\Plugin;
|
||||||
|
use Filament\Panel;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource;
|
||||||
|
|
||||||
|
class DnsToolsPlugin implements Plugin
|
||||||
|
{
|
||||||
|
public static function make(): static
|
||||||
|
{
|
||||||
|
return app(static::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'dns-tools';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(Panel $panel): void
|
||||||
|
{
|
||||||
|
$panel->resources([
|
||||||
|
DnsToolsResource::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$widgetPath = __DIR__ . '/Filament/Widgets';
|
||||||
|
if (is_dir($widgetPath)) {
|
||||||
|
$widgetClasses = $this->discoverWidgets($widgetPath);
|
||||||
|
$panel->widgets($widgetClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(Panel $panel): void {}
|
||||||
|
|
||||||
|
protected function discoverWidgets(string $path): array
|
||||||
|
{
|
||||||
|
$widgets = [];
|
||||||
|
|
||||||
|
/*
|
||||||
|
$files = File::allFiles($path);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$class = 'VEximweb\\Plugin\\DMARC\\Filament\\Widgets\\' . $file->getFilenameWithoutExtension();
|
||||||
|
if (class_exists($class)) {
|
||||||
|
$widgets[] = $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
return $widgets;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use VEximweb\Plugin\DnsTools\Services\DnsToolsService;
|
||||||
|
use VEximweb\Plugin\DnsTools\Dmarc\Services\DmarcCheckService;
|
||||||
|
use Filament\Panel;
|
||||||
|
//use VEximweb\Plugin\DnsTools\Filament\Providers\DnsToolsPanelProvider;
|
||||||
|
|
||||||
|
class DnsToolsServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Merge config
|
||||||
|
/*
|
||||||
|
$this->mergeConfigFrom(
|
||||||
|
__DIR__ . '/../../config/dmarc.php',
|
||||||
|
'dmarc'
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Register DMARC record service
|
||||||
|
$this->app->singleton('dmarc.record.service', function ($app) {
|
||||||
|
// Try to get setting repository from container
|
||||||
|
$settingRepository = null;
|
||||||
|
try {
|
||||||
|
if ($app->has('VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface')) {
|
||||||
|
$settingRepository = $app->make('VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface');
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Setting repository not available, continue without it
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DmarcRecordService($settingRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register main service
|
||||||
|
$this->app->singleton('dns-tools.service', function ($app) {
|
||||||
|
return new DnsToolsService();
|
||||||
|
});
|
||||||
|
|
||||||
|
Panel::configureUsing(function (Panel $panel) {
|
||||||
|
$panel->plugin(DnsToolsPlugin::make());
|
||||||
|
});
|
||||||
|
|
||||||
|
//$this->app->register(DnsToolsPanelProvider::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__ . '/../../config/dmarc.php' => config_path('dmarc.php'),
|
||||||
|
], 'dmarc-config');
|
||||||
|
|
||||||
|
// Load migrations
|
||||||
|
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
||||||
|
|
||||||
|
// Load routes
|
||||||
|
// $this->loadRoutesFrom(__DIR__ . '/../../routes/web.php');
|
||||||
|
|
||||||
|
// Load views
|
||||||
|
$this->loadViewsFrom(__DIR__ . '/../../resources/views', 'dns-tools');
|
||||||
|
|
||||||
|
// Load translations
|
||||||
|
//$this->loadTranslationsFrom(__DIR__ . '/../../resources/lang', 'dns-tools');
|
||||||
|
|
||||||
|
|
||||||
|
if (class_exists(\Filament\Panel::class)) {
|
||||||
|
// The resource will be auto-discovered if you use:
|
||||||
|
// $panel->discoverResources() in your panel provider
|
||||||
|
// Or register it manually:
|
||||||
|
\Filament\Facades\Filament::registerResources([
|
||||||
|
\VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
$this->commands([
|
||||||
|
\VEximweb\Plugin\DnsTools\Console\Commands\CheckDmarcRecords::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace VEximweb\Plugin\DnsTools\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DnsToolsService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check MX records for a domain
|
||||||
|
*
|
||||||
|
* @param string $domain
|
||||||
|
* @return array|null Array of MX records or null if none found
|
||||||
|
*/
|
||||||
|
public function checkMxRecord(string $domain): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$mxRecords = dns_get_record($domain, DNS_MX);
|
||||||
|
|
||||||
|
if (empty($mxRecords)) {
|
||||||
|
Log::info("No MX records found for domain: {$domain}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority
|
||||||
|
usort($mxRecords, function($a, $b) {
|
||||||
|
return $a['pri'] <=> $b['pri'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $mxRecords;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Error checking MX records for {$domain}: " . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if domain has valid MX records
|
||||||
|
*/
|
||||||
|
public function hasValidMxRecord(string $domain): bool
|
||||||
|
{
|
||||||
|
$records = $this->checkMxRecord($domain);
|
||||||
|
return !empty($records);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSPFRecord(string $domain) {
|
||||||
|
$dnsRecords = dns_get_record($domain, DNS_TXT);
|
||||||
|
|
||||||
|
if (!$dnsRecords) {
|
||||||
|
return "Failed to fetch DNS records.";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dnsRecords as $record) {
|
||||||
|
if (isset($record['txt']) && stripos($record['txt'], 'v=spf1') === 0) {
|
||||||
|
return $record['txt'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "No SPF record found.";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user