diff --git a/Freewood-OctoWoW-Mail.toc b/Freewood-OctoWoW-Mail.toc
new file mode 100644
index 0000000..6f36fb5
--- /dev/null
+++ b/Freewood-OctoWoW-Mail.toc
@@ -0,0 +1,12 @@
+## Interface: 11200
+## Title: Freewood |cffc89b3cMail|r
+## Notes: Own mailbox window: open all or selected mails safely, recent recipients, mailing lists with automatic sending, multi-item mails, gold surplus and cash on delivery.
+## Notes-deDE: Eigenes Briefkasten-Fenster: alles oder Ausgewähltes sicher öffnen, letzte Empfänger, Mailinglisten mit Automatik, mehrere Items, Gold-Überschuss und Nachnahme.
+## Author: Lacustat
+## Version: 0.1.152
+## Dependencies: Freewood-OctoWoW-Core
+## OptionalDeps: Freewood-OctoWoW-Creator, Freewood-OctoWoW-Brett
+
+Locales.lua
+Mail.lua
+MailUI.lua
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ 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.
+
+
+ Copyright (C)
+
+ 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 .
+
+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:
+
+ Copyright (C)
+ 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
+.
+
+ 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
+.
diff --git a/Locales.lua b/Locales.lua
new file mode 100644
index 0000000..a9aaa3c
--- /dev/null
+++ b/Locales.lua
@@ -0,0 +1,205 @@
+-- Freewood-OctoWoW-Mail: Texte
+local ADDON, NS = ...
+
+local FW = _G.Freewood
+if not FW then
+ return
+end
+
+FW:AddLocale("enUS", {
+ MAIL_TITLE = "Mailbox",
+ MAIL_DESC = "Own mailbox window: open all or selected mails safely, recent recipients, mailing lists, several items per mail, gold surplus and cash on delivery.",
+ MAIL_WINDOW_TITLE = "Freewood Mailbox",
+ MAIL_TAB_INBOX = "Inbox",
+ MAIL_TAB_SEND = "Send",
+ MAIL_TAB_LISTS = "Mailing lists",
+ MAIL_CLOSE = "Close mailbox",
+
+ MAIL_INBOX_COUNT = "%d mails in the inbox",
+ MAIL_OPEN_ALL = "Open all",
+ MAIL_OPEN_SELECTED = "Open selected only (%d)",
+ MAIL_STOP = "Stop",
+ MAIL_SELECT_ALL = "Select all",
+ MAIL_SELECT_NONE = "Select none",
+ MAIL_INBOX_EMPTY = "The inbox is empty.",
+ MAIL_ROW_GOLD = "Gold: %s",
+ MAIL_ROW_COD = "Cash on delivery: %s (not taken)",
+ MAIL_ROW_LETTER = "Letter only (not touched)",
+ MAIL_ROW_DAYS = "%d d",
+ MAIL_ROW_FROM = "From %s",
+ MAIL_NOTE_SAFE = "Mails are never deleted or returned. Cash-on-delivery mails and plain letters are skipped. One bag slot always stays free.",
+ MAIL_RUN_DONE = "Mailbox: %d gold mails and %d items taken%s.",
+ MAIL_RUN_SKIP_FULL = ", %d items left because the bags are nearly full",
+ MAIL_RUN_STOPPED = "Mailbox: stopped. %d gold mails and %d items taken.",
+ MAIL_RUN_PROGRESS = "Taking mail... %d items, %d gold mails",
+ MAIL_RUN_FULL = "Bags nearly full: items stay in the mailbox (one slot stays free).",
+
+ MAIL_TO = "Recipient",
+ MAIL_TO_RECENT = "Last recipients",
+ MAIL_TO_NONE = "No recipients yet",
+ MAIL_SUBJECT = "Subject",
+ MAIL_BODY = "Text",
+ MAIL_ITEMS = "Items",
+ MAIL_ITEMS_ADD = "Add items...",
+ MAIL_ITEMS_NONE = "No items. Without items only gold (if any) is sent.",
+ MAIL_ITEMS_REMOVE = "Remove",
+ MAIL_GOLD = "Gold",
+ MAIL_SILVER = "Silver",
+ MAIL_COPPER = "Copper",
+ MAIL_COD = "Cash on delivery (the recipient pays this amount for every mail)",
+ MAIL_SEND = "Send",
+ MAIL_SEND_COUNT = "%d mails will be sent one after another.",
+ MAIL_SEND_COUNT1 = "1 mail will be sent.",
+ MAIL_SEND_NOTHING = "Nothing to send: add items or gold.",
+ MAIL_SEND_NO_TO = "Enter a recipient.",
+ MAIL_SEND_SELF = "You cannot mail yourself.",
+ MAIL_SEND_COD_NEEDS_ITEM = "Cash on delivery needs at least one item.",
+ MAIL_BOARD_BUSY = "A board mission is preparing a mail. Finish it in the mission window first; this window waits.",
+ MAIL_BUSY = "Another action is running. Please wait.",
+
+ MAIL_PICK_TITLE = "Choose items",
+ MAIL_PICK_LIST_TITLE = "Items for %s",
+ MAIL_PICK_DONE = "Done",
+ MAIL_PICK_BAGS = "In your bags",
+ MAIL_PICK_EXTRA = "Not in your bags",
+ MAIL_PICK_EMPTY = "Nothing to choose in your bags.",
+ MAIL_PICK_HINT_LIST = "All stacks of a chosen item are sent. Click again to remove.",
+ MAIL_PICK_HINT_SEND = "Every chosen stack is one mail. Click again to remove.",
+
+ MAIL_LISTS_HEADER = "Mailing lists",
+ MAIL_LISTS_HINT = "Create a name, choose its items, and tick the box to send automatically whenever the mailbox opens.",
+ MAIL_LISTS_ADD = "Add",
+ MAIL_LISTS_NAME_BOX = "Character name",
+ MAIL_LISTS_EMPTY = "No mailing list yet.",
+ MAIL_LISTS_ITEMS = "Items (%d)",
+ MAIL_LISTS_AUTO = "Send automatically when the mailbox opens",
+ MAIL_LISTS_GOLD = "Send gold above",
+ MAIL_LISTS_GOLD_G = "gold (keep this much)",
+ MAIL_LISTS_NOW = "Send now",
+ MAIL_LISTS_REMOVE = "Remove list",
+ MAIL_LISTS_SELF = "This is you; it is skipped on this character.",
+ MAIL_LISTS_RESERVED = "%d items reserved for board missions",
+ MAIL_LISTS_ALL_NOW = "Send all lists now",
+
+ MAIL_SUBJECT_STAMP = "%s",
+ MAIL_SUBJECT_N = "%s - %d of %d",
+ MAIL_SUBJECT_ITEM_N = "%s (%d of %d)",
+ MAIL_SUBJECT_DEFAULT = "Freewood",
+
+ MAIL_SEND_PROGRESS = "Sending mail %d of %d to %s...",
+ MAIL_SEND_DONE = "Mail: %d of %d mails sent%s.",
+ MAIL_SEND_DONE_FAIL = ", %d skipped",
+ MAIL_SEND_STOPPED = "Mail: sending stopped after %d of %d mails.",
+ MAIL_SEND_NOTHING_LIST = "Mail: nothing to send for %s.",
+ MAIL_SEND_FAIL_ATTACH = "Mail: %s could not be attached (soulbound or locked?). Skipped.",
+ MAIL_SEND_FAIL_TIMEOUT = "Mail: no answer from the server. Sending stopped.",
+ MAIL_SEND_FAIL_ERROR = "Mail: the server refused a mail. Sending stopped.",
+ MAIL_AUTO_WAIT_BOARD = "Mail: automatic sending waits for the board mission mail.",
+
+ MAIL_SET_HEADER = "Mailbox",
+ MAIL_SET_REPLACE = "Replace the normal mailbox window with the Freewood mailbox",
+ MAIL_SET_CHAT = "Report results in chat",
+ MAIL_SET_NOTE = "Off by default. With the replacement on, the normal window stays open but invisible, so board missions keep working. /fwmail off switches back at any time.",
+ MAIL_CMD_ON = "Freewood mailbox window: on",
+ MAIL_CMD_OFF = "Freewood mailbox window: off",
+ MAIL_CMD_CLOSED = "Open a mailbox first.",
+})
+
+FW:AddLocale("deDE", {
+ MAIL_TITLE = "Briefkasten",
+ MAIL_DESC = "Eigenes Briefkasten-Fenster: alles oder Ausgewähltes sicher öffnen, letzte Empfänger, Mailinglisten, mehrere Items pro Versand, Gold-Überschuss und Nachnahme.",
+ MAIL_WINDOW_TITLE = "Freewood Briefkasten",
+ MAIL_TAB_INBOX = "Posteingang",
+ MAIL_TAB_SEND = "Senden",
+ MAIL_TAB_LISTS = "Mailinglisten",
+ MAIL_CLOSE = "Briefkasten schließen",
+
+ MAIL_INBOX_COUNT = "%d Briefe im Posteingang",
+ MAIL_OPEN_ALL = "Alle öffnen",
+ MAIL_OPEN_SELECTED = "Nur Ausgewählte öffnen (%d)",
+ MAIL_STOP = "Stopp",
+ MAIL_SELECT_ALL = "Alle auswählen",
+ MAIL_SELECT_NONE = "Keine auswählen",
+ MAIL_INBOX_EMPTY = "Der Posteingang ist leer.",
+ MAIL_ROW_GOLD = "Gold: %s",
+ MAIL_ROW_COD = "Nachnahme: %s (wird nicht genommen)",
+ MAIL_ROW_LETTER = "Nur ein Brief (bleibt unberührt)",
+ MAIL_ROW_DAYS = "%d T",
+ MAIL_ROW_FROM = "Von %s",
+ MAIL_NOTE_SAFE = "Briefe werden nie gelöscht oder zurückgeschickt. Nachnahme-Briefe und reine Briefe werden übersprungen. Ein Taschenplatz bleibt immer frei.",
+ MAIL_RUN_DONE = "Briefkasten: %d Gold-Briefe und %d Items genommen%s.",
+ MAIL_RUN_SKIP_FULL = ", %d Items blieben liegen, weil die Taschen fast voll sind",
+ MAIL_RUN_STOPPED = "Briefkasten: gestoppt. %d Gold-Briefe und %d Items genommen.",
+ MAIL_RUN_PROGRESS = "Nehme Post... %d Items, %d Gold-Briefe",
+ MAIL_RUN_FULL = "Taschen fast voll: Items bleiben im Briefkasten (ein Platz bleibt frei).",
+
+ MAIL_TO = "Empfänger",
+ MAIL_TO_RECENT = "Letzte Empfänger",
+ MAIL_TO_NONE = "Noch keine Empfänger",
+ MAIL_SUBJECT = "Betreff",
+ MAIL_BODY = "Text",
+ MAIL_ITEMS = "Items",
+ MAIL_ITEMS_ADD = "Items hinzufügen...",
+ MAIL_ITEMS_NONE = "Keine Items. Ohne Items wird nur Gold (falls angegeben) gesendet.",
+ MAIL_ITEMS_REMOVE = "Entfernen",
+ MAIL_GOLD = "Gold",
+ MAIL_SILVER = "Silber",
+ MAIL_COPPER = "Kupfer",
+ MAIL_COD = "Nachnahme (der Empfänger zahlt diesen Betrag für jeden Brief)",
+ MAIL_SEND = "Senden",
+ MAIL_SEND_COUNT = "%d Briefe werden nacheinander gesendet.",
+ MAIL_SEND_COUNT1 = "1 Brief wird gesendet.",
+ MAIL_SEND_NOTHING = "Nichts zu senden: Items oder Gold angeben.",
+ MAIL_SEND_NO_TO = "Empfänger eingeben.",
+ MAIL_SEND_SELF = "Du kannst dir selbst nichts schicken.",
+ MAIL_SEND_COD_NEEDS_ITEM = "Nachnahme braucht mindestens ein Item.",
+ MAIL_BOARD_BUSY = "Ein Brett-Auftrag bereitet gerade einen Brief vor. Erst im Auftragsfenster abschließen; dieses Fenster wartet.",
+ MAIL_BUSY = "Es läuft gerade eine andere Aktion. Bitte warten.",
+
+ MAIL_PICK_TITLE = "Items auswählen",
+ MAIL_PICK_LIST_TITLE = "Items für %s",
+ MAIL_PICK_DONE = "Fertig",
+ MAIL_PICK_BAGS = "In deinen Taschen",
+ MAIL_PICK_EXTRA = "Nicht in den Taschen",
+ MAIL_PICK_EMPTY = "In deinen Taschen gibt es nichts zum Auswählen.",
+ MAIL_PICK_HINT_LIST = "Alle Stapel eines gewählten Items werden gesendet. Nochmal klicken entfernt es.",
+ MAIL_PICK_HINT_SEND = "Jeder gewählte Stapel ist ein Brief. Nochmal klicken entfernt ihn.",
+
+ MAIL_LISTS_HEADER = "Mailinglisten",
+ MAIL_LISTS_HINT = "Namen anlegen, Items festlegen und mit dem Haken automatisch senden, sobald der Briefkasten aufgeht.",
+ MAIL_LISTS_ADD = "Anlegen",
+ MAIL_LISTS_NAME_BOX = "Charaktername",
+ MAIL_LISTS_EMPTY = "Noch keine Mailingliste.",
+ MAIL_LISTS_ITEMS = "Items (%d)",
+ MAIL_LISTS_AUTO = "Automatisch senden, sobald der Briefkasten aufgeht",
+ MAIL_LISTS_GOLD = "Gold senden über",
+ MAIL_LISTS_GOLD_G = "Gold (so viel behalten)",
+ MAIL_LISTS_NOW = "Jetzt senden",
+ MAIL_LISTS_REMOVE = "Liste entfernen",
+ MAIL_LISTS_SELF = "Das bist du; auf diesem Charakter wird sie übersprungen.",
+ MAIL_LISTS_RESERVED = "%d Items sind für Brett-Aufträge reserviert",
+ MAIL_LISTS_ALL_NOW = "Alle Listen jetzt senden",
+
+ MAIL_SUBJECT_STAMP = "%s",
+ MAIL_SUBJECT_N = "%s - %d von %d",
+ MAIL_SUBJECT_ITEM_N = "%s (%d von %d)",
+ MAIL_SUBJECT_DEFAULT = "Freewood",
+
+ MAIL_SEND_PROGRESS = "Sende Brief %d von %d an %s...",
+ MAIL_SEND_DONE = "Briefkasten: %d von %d Briefen gesendet%s.",
+ MAIL_SEND_DONE_FAIL = ", %d übersprungen",
+ MAIL_SEND_STOPPED = "Briefkasten: Senden nach %d von %d Briefen gestoppt.",
+ MAIL_SEND_NOTHING_LIST = "Briefkasten: Für %s gibt es nichts zu senden.",
+ MAIL_SEND_FAIL_ATTACH = "Briefkasten: %s ließ sich nicht anhängen (seelengebunden oder gesperrt?). Übersprungen.",
+ MAIL_SEND_FAIL_TIMEOUT = "Briefkasten: keine Antwort vom Server. Senden gestoppt.",
+ MAIL_SEND_FAIL_ERROR = "Briefkasten: Der Server hat einen Brief abgelehnt. Senden gestoppt.",
+ MAIL_AUTO_WAIT_BOARD = "Briefkasten: Das automatische Senden wartet auf den Brief des Brett-Auftrags.",
+
+ MAIL_SET_HEADER = "Briefkasten",
+ MAIL_SET_REPLACE = "Normales Briefkasten-Fenster durch das Freewood-Briefkasten ersetzen",
+ MAIL_SET_CHAT = "Ergebnisse im Chat melden",
+ MAIL_SET_NOTE = "Standardmäßig aus. Mit Ersetzen bleibt das normale Fenster geöffnet, aber unsichtbar, damit Brett-Aufträge weiter funktionieren. /fwmail off schaltet jederzeit zurück.",
+ MAIL_CMD_ON = "Freewood-Briefkasten-Fenster: an",
+ MAIL_CMD_OFF = "Freewood-Briefkasten-Fenster: aus",
+ MAIL_CMD_CLOSED = "Öffne zuerst einen Briefkasten.",
+})
diff --git a/Mail.lua b/Mail.lua
new file mode 100644
index 0000000..a2f0b52
--- /dev/null
+++ b/Mail.lua
@@ -0,0 +1,800 @@
+-- Freewood-OctoWoW-Mail: eigenes Briefkasten (Logik).
+--
+-- Posteingang "Alle oeffnen" / "Nur Ausgewaehlte oeffnen": nimmt Gold und Items, ein Schritt nach dem anderen.
+-- - loescht und retourniert nie einen Brief; Nachnahme-Briefe und reine Briefe bleiben unberuehrt
+-- - ein Taschenplatz bleibt immer frei (Items bleiben sonst im Briefkasten)
+-- - nach jedem Schritt wird der Posteingang neu gelesen (die Nummern verschieben sich)
+-- Senden Empfaenger mit den letzten 5, mehrere Items (je Item ein Brief, nacheinander), Gold, Nachnahme
+-- Mailinglisten Namen mit Items, optional Gold-Ueberschuss und Automatik beim Oeffnen des Briefkastens
+--
+-- Alle Sendungen laufen ueber dieselbe Warteschlange (Mail.send): ein Brief, dann auf MAIL_SEND_SUCCESS warten,
+-- dann Pause, dann der naechste. Sie laufen nie, solange ein Brett-Auftrag einen Brief vorbereitet (BoardBusy).
+--
+-- Gespeichert: Account-weit Mail.db = { settings, recipients, lists }.
+
+local ADDON, NS = ...
+
+local FW = _G.Freewood
+if not FW or not FW:RequireAPI(1, ADDON) then
+ return
+end
+
+local Util = FW.Util
+local L = FW.L
+
+local MAX_RECENT = 5
+local MAX_LISTS = 30
+local MAX_ITEMS_PER_LIST = 60
+local MAX_JOBS = 120
+local STEP = 0.1 -- Takt des Treibers
+local INBOX_GAP = 0.5 -- Pause nach dem Nehmen
+local INBOX_TIMEOUT = 2.5 -- so lange auf die Antwort des Servers warten
+local SEND_GAP = 1.2 -- Pause nach jedem gesendeten Brief
+local SEND_TIMEOUT = 8
+local POSTAGE = 30 -- Kupfer je Brief (Schaetzung fuer den Gold-Ueberschuss)
+local AUTO_DELAY = 2.5
+local KEEP_FREE = 1 -- so viele Taschenplaetze bleiben beim Oeffnen immer frei
+
+local function GetMeta(field)
+ local fn = (C_AddOns and C_AddOns.GetAddOnMetadata) or GetAddOnMetadata
+ if fn then
+ local ok, value = pcall(fn, ADDON, field)
+ if ok then
+ return value
+ end
+ end
+ return nil
+end
+
+local Mail = FW:NewModule("Mail", {
+ version = GetMeta("Version") or "dev",
+ title = L.MAIL_TITLE,
+ description = L.MAIL_DESC,
+ defaults = {
+ settings = { replace = false, chat = true },
+ recipients = {},
+ lists = {},
+ },
+})
+NS.Mail = Mail
+FW.Mail = Mail
+
+Mail.open = false -- ein Briefkasten ist offen
+Mail.busy = nil -- nil | "inbox" | "send"
+Mail.gen = 0 -- zaehlt die Briefkasten-Oeffnungen
+
+local function Now() return GetTime and GetTime() or 0 end
+local function After(t, fn)
+ if C_Timer and C_Timer.After then C_Timer.After(t, fn) else pcall(fn) end
+end
+
+local function Trace(msg)
+ local C = FW.Creator
+ if C and C.Trace then pcall(C.Trace, "Mail: " .. tostring(msg)) end
+end
+Mail.Trace = Trace
+
+local function Say(msg)
+ if Mail.db and Mail.db.settings.chat and FW.Print then FW:Print(msg) end
+end
+
+function Mail.Money(c)
+ if Util.FormatMoney then return Util.FormatMoney(c) end
+ return tostring(c) .. "c"
+end
+
+local function Trim(s)
+ return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
+end
+
+-- Charaktername: Leerzeichen weg, erster Buchstabe gross
+function Mail.CleanName(s)
+ s = Trim(s):gsub("%s+", "")
+ if s == "" then return "" end
+ return (s:sub(1, 1):upper() .. s:sub(2))
+end
+
+local function IsSelf(name)
+ local me = UnitName and UnitName("player")
+ return me and name and name:lower() == me:lower()
+end
+Mail.IsSelf = IsSelf
+
+------------------------------------------------------------------------
+-- Taschen
+------------------------------------------------------------------------
+
+local function NumSlots(bag)
+ local fn = (C_Container and C_Container.GetContainerNumSlots) or GetContainerNumSlots
+ if type(fn) ~= "function" then return 0 end
+ local ok, n = pcall(fn, bag)
+ return (ok and tonumber(n)) or 0
+end
+
+local function SlotInfo(bag, slot)
+ if C_Container and C_Container.GetContainerItemInfo then
+ local ok, info = pcall(C_Container.GetContainerItemInfo, bag, slot)
+ if ok and type(info) == "table" then
+ return tonumber(info.itemID), tonumber(info.stackCount) or 1, info.isLocked and true or false
+ end
+ end
+ if GetContainerItemInfo then
+ local ok, _, count, locked, _, _, _, link, _, _, itemID = pcall(GetContainerItemInfo, bag, slot)
+ if ok then
+ local id = tonumber(itemID) or (type(link) == "string" and tonumber(link:match("item:(%d+)"))) or nil
+ if not id and GetContainerItemLink then
+ local ok2, l2 = pcall(GetContainerItemLink, bag, slot)
+ if ok2 and type(l2) == "string" then id = tonumber(l2:match("item:(%d+)")) end
+ end
+ return id, tonumber(count) or 1, locked and true or false
+ end
+ end
+ return nil
+end
+
+local function BagCount() return tonumber(_G.NUM_BAG_SLOTS) or 4 end
+
+-- Alle Staepel in den normalen Taschen: { bag, slot, id, count, locked }
+function Mail.BagStacks()
+ local out = {}
+ for bag = 0, BagCount() do
+ for slot = 1, NumSlots(bag) do
+ local id, count, locked = SlotInfo(bag, slot)
+ if id then out[#out + 1] = { bag = bag, slot = slot, id = id, count = count, locked = locked } end
+ end
+ end
+ return out
+end
+
+-- freie Plaetze in Taschen ohne Spezialtyp (Koecher, Seelentasche ... zaehlen nicht)
+function Mail.FreeSlots()
+ local fn = (C_Container and C_Container.GetContainerNumFreeSlots) or GetContainerNumFreeSlots
+ if type(fn) ~= "function" then return 0 end
+ local n = 0
+ for bag = 0, BagCount() do
+ local ok, free, kind = pcall(fn, bag)
+ if ok and tonumber(free) and (kind == nil or kind == 0) then n = n + free end
+ end
+ return n
+end
+
+------------------------------------------------------------------------
+-- Zusammenspiel mit den Brett-Auftraegen (Creator)
+------------------------------------------------------------------------
+
+-- Ein Brett-Auftrag bereitet gerade einen Brief vor oder fragt danach: dann fassen wir den Brief nicht an.
+function Mail.BoardBusy()
+ local C = FW.Creator
+ if not C then return false end
+ if C.mailChoice or C.asking then return true end
+ if C.askQueue and #C.askQueue > 0 then return true end
+ if C.GuardState then
+ local ok, st = pcall(C.GuardState)
+ if ok and st and st ~= "off" then return true end
+ end
+ return false
+end
+
+-- Items, die ein offener Brett-Auftrag noch verschicken muss: set[itemID] = true
+function Mail.Reserved()
+ local set, n = {}, 0
+ local C = FW.Creator
+ if C and C.ActiveList and C.db and C.ObjProgress then
+ local ok = pcall(function()
+ for _, id in ipairs(C.ActiveList()) do
+ local q = C.db.quests[id]
+ for i, o in ipairs(q and q.objs or {}) do
+ if o.type == "mail" and o.item then
+ local have, need = C.ObjProgress(id, i)
+ if have < need and not set[o.item] then
+ set[o.item] = true
+ n = n + 1
+ end
+ end
+ end
+ end
+ end)
+ if not ok then Trace("Reservierung konnte nicht gelesen werden") end
+ end
+ return set, n
+end
+
+------------------------------------------------------------------------
+-- Empfaenger (die letzten 5)
+------------------------------------------------------------------------
+
+function Mail:AddRecipient(name)
+ name = Mail.CleanName(name)
+ if name == "" or IsSelf(name) then return end
+ local list = self.db.recipients
+ for i = #list, 1, -1 do
+ if list[i]:lower() == name:lower() then table.remove(list, i) end
+ end
+ table.insert(list, 1, name)
+ while #list > MAX_RECENT do table.remove(list) end
+ FW:Fire("MAIL_RECIPIENTS_CHANGED")
+end
+
+function Mail:Recipients()
+ return self.db.recipients
+end
+
+------------------------------------------------------------------------
+-- Posteingang
+------------------------------------------------------------------------
+
+-- Liste der Briefe: { i, sender, subject, money, cod, days, hasItem, itemName, itemTex, itemCount, sig, key, take, why }
+function Mail.ReadInbox()
+ local list = {}
+ if not GetInboxNumItems then return list end
+ local ok, n = pcall(GetInboxNumItems)
+ n = (ok and tonumber(n)) or 0
+ local seen = {}
+ for i = 1, n do
+ local ok1, _, _, sender, subject, money, cod, days, hasItem, wasRead = pcall(GetInboxHeaderInfo, i)
+ if ok1 then
+ local m = {
+ i = i, sender = sender or "?", subject = subject or "",
+ money = tonumber(money) or 0, cod = tonumber(cod) or 0, days = tonumber(days),
+ hasItem = (hasItem and hasItem ~= 0) and true or false, read = wasRead and true or false,
+ }
+ if m.hasItem and GetInboxItem then
+ local ok2, name, tex, count = pcall(GetInboxItem, i)
+ if ok2 then m.itemName, m.itemTex, m.itemCount = name, tex, tonumber(count) or 1 end
+ end
+ m.sig = table.concat({ m.sender, m.subject, m.money, m.cod, tostring(m.itemName), tostring(m.itemCount) }, "|")
+ seen[m.sig] = (seen[m.sig] or 0) + 1
+ m.key = m.sig .. "#" .. seen[m.sig]
+ if m.cod > 0 then
+ m.take, m.why = false, "cod"
+ elseif m.money > 0 or m.hasItem then
+ m.take = true
+ else
+ m.take, m.why = false, "letter"
+ end
+ list[#list + 1] = m
+ end
+ end
+ return list
+end
+
+Mail.selected = {} -- key -> true
+
+function Mail:SelectedCount()
+ local n = 0
+ for _ in pairs(self.selected) do n = n + 1 end
+ return n
+end
+
+function Mail:ToggleSelect(key, on)
+ if on == nil then on = not self.selected[key] end
+ self.selected[key] = on and true or nil
+ FW:Fire("MAIL_SELECTION_CHANGED")
+end
+
+function Mail:SelectAll(on)
+ self.selected = {}
+ if on then
+ for _, m in ipairs(Mail.ReadInbox()) do
+ if m.take then self.selected[m.key] = true end
+ end
+ end
+ FW:Fire("MAIL_SELECTION_CHANGED")
+end
+
+-- onlySelected: nur angehakte Briefe
+function Mail:StartOpen(onlySelected)
+ if self.busy or not self.open then return false end
+ local only
+ if onlySelected then
+ only = {}
+ for k in pairs(self.selected) do only[k] = true end
+ if next(only) == nil then return false end
+ end
+ self.run = { only = only, tries = {}, money = 0, items = 0, pending = nil }
+ self.busy = "inbox"
+ self.driver:Show()
+ Trace("Posteingang: Start" .. (only and " (nur Ausgewaehlte)" or " (alle)"))
+ FW:Fire("MAIL_STATE")
+ return true
+end
+
+local function FinishOpen(self, reason)
+ local run = self.run
+ self.run, self.busy = nil, nil
+ if not run then return end
+ local skipped = 0
+ if run.full and reason == "done" then
+ for _, m in ipairs(Mail.ReadInbox()) do
+ if m.take and m.hasItem and (not run.only or run.only[m.key]) then skipped = skipped + 1 end
+ end
+ end
+ Trace(string.format("Posteingang: Ende (%s), %d Gold-Briefe, %d Items, %d Items liegen geblieben", reason, run.money, run.items, skipped))
+ if reason == "stopped" or reason == "closed" then
+ Say(string.format(FW.L.MAIL_RUN_STOPPED, run.money, run.items))
+ else
+ Say(string.format(FW.L.MAIL_RUN_DONE, run.money, run.items, skipped > 0 and string.format(FW.L.MAIL_RUN_SKIP_FULL, skipped) or ""))
+ end
+ self.lastRun = { money = run.money, items = run.items, skipped = skipped }
+ self.selected = {}
+ FW:Fire("MAIL_STATE")
+ FW:Fire("MAIL_SELECTION_CHANGED")
+ FW:Fire("MAIL_INBOX_CHANGED")
+end
+
+function Mail:Stop()
+ if self.busy == "inbox" and self.run then FinishOpen(self, "stopped") end
+ if self.busy == "send" and self.send then self:FinishSend("stopped") end
+end
+
+function Mail:StepOpen()
+ local run = self.run
+ local now = Now()
+ if not self.open then return FinishOpen(self, "closed") end
+ if run.pending then
+ -- auf die Antwort des Servers warten (oder den Ablauf der Zeit)
+ if not ((run.updated and now >= run.pending + INBOX_GAP) or now >= run.pending + INBOX_TIMEOUT) then return end
+ run.pending = nil
+ end
+ local list = Mail.ReadInbox()
+ local free = Mail.FreeSlots()
+ for idx = #list, 1, -1 do
+ local m = list[idx]
+ if m.take and (not run.only or run.only[m.key]) and (run.tries[m.sig] or 0) < 3 then
+ if m.money > 0 then
+ run.tries[m.sig] = (run.tries[m.sig] or 0) + 1
+ run.money = run.money + 1
+ run.pending, run.updated = now, false
+ pcall(TakeInboxMoney, m.i)
+ Trace(string.format("Brief %d von %s: Gold %d genommen", m.i, m.sender, m.money))
+ FW:Fire("MAIL_STATE")
+ return
+ elseif m.hasItem then
+ if free <= KEEP_FREE then
+ run.full = true
+ else
+ run.tries[m.sig] = (run.tries[m.sig] or 0) + 1
+ run.items = run.items + 1
+ run.pending, run.updated = now, false
+ pcall(TakeInboxItem, m.i)
+ Trace(string.format("Brief %d von %s: Item %s genommen (frei vorher %d)", m.i, m.sender, tostring(m.itemName), free))
+ FW:Fire("MAIL_STATE")
+ return
+ end
+ end
+ end
+ end
+ FinishOpen(self, "done")
+end
+
+------------------------------------------------------------------------
+-- Senden (gemeinsame Warteschlange)
+------------------------------------------------------------------------
+
+local function AttachedName()
+ if not GetSendMailItem then return nil end
+ local ok, name = pcall(GetSendMailItem, 1)
+ if ok and name and name ~= "" then return name end
+ return nil
+end
+
+-- Anhang-Platz leeren (zurueck in die Tasche)
+local function ClearAttachment()
+ if AttachedName() and ClickSendMailItemButton then
+ pcall(ClickSendMailItemButton)
+ if CursorHasItem and CursorHasItem() then pcall(ClearCursor) end
+ end
+end
+
+local function FindStack(item)
+ local fallback
+ for _, s in ipairs(Mail.BagStacks()) do
+ if s.id == item.id and not s.locked then
+ if s.count == item.count then return s end
+ fallback = fallback or s
+ end
+ end
+ return fallback
+end
+
+-- Bereitet einen Brief vor. Rueckgabe: true oder false, Grund
+local function Prepare(job)
+ if CursorHasItem and CursorHasItem() then return false, "cursor" end
+ ClearAttachment()
+ if job.item then
+ local s = FindStack(job.item)
+ if not s then return false, "gone" end
+ pcall(ClearCursor)
+ pcall(PickupContainerItem, s.bag, s.slot)
+ if not (CursorHasItem and CursorHasItem()) then return false, "pickup" end
+ if not ClickSendMailItemButton then pcall(ClearCursor) return false, "noapi" end
+ pcall(ClickSendMailItemButton)
+ if not AttachedName() then
+ pcall(ClearCursor)
+ return false, "attach"
+ end
+ end
+ if SetSendMailMoney then pcall(SetSendMailMoney, (job.cod or 0) > 0 and 0 or (job.money or 0)) end
+ if SetSendMailCOD then pcall(SetSendMailCOD, job.cod or 0) end
+ return true
+end
+
+local function JobLabel(job)
+ if job.item then
+ local info = Util.GetItem and Util.GetItem(job.item.id)
+ return (info and info.name) or ("#" .. tostring(job.item.id))
+ end
+ return Mail.Money(job.money or 0)
+end
+
+-- jobs: { to, subject, body, item = {id,count}, money, cod }
+function Mail:StartSend(jobs)
+ if self.busy then return false, "busy" end
+ if not self.open then return false, "closed" end
+ if Mail.BoardBusy() then return false, "board" end
+ if not jobs or #jobs == 0 then return false, "empty" end
+ self.send = { jobs = jobs, i = 0, sent = 0, failed = 0, stage = "next", nextAt = 0 }
+ self.busy = "send"
+ self.driver:Show()
+ Trace(string.format("Senden: Start mit %d Briefen", #jobs))
+ FW:Fire("MAIL_STATE")
+ return true
+end
+
+function Mail:FinishSend(reason)
+ local s = self.send
+ self.send, self.busy = nil, nil
+ if not s then return end
+ pcall(ClearCursor)
+ if reason ~= "done" then ClearAttachment() end
+ Trace(string.format("Senden: Ende (%s), %d von %d gesendet, %d uebersprungen", reason, s.sent, #s.jobs, s.failed))
+ if reason == "done" then
+ Say(string.format(FW.L.MAIL_SEND_DONE, s.sent, #s.jobs, s.failed > 0 and string.format(FW.L.MAIL_SEND_DONE_FAIL, s.failed) or ""))
+ elseif reason == "timeout" then
+ Say(FW.L.MAIL_SEND_FAIL_TIMEOUT)
+ elseif reason == "error" then
+ Say(FW.L.MAIL_SEND_FAIL_ERROR)
+ else
+ Say(string.format(FW.L.MAIL_SEND_STOPPED, s.sent, #s.jobs))
+ end
+ FW:Fire("MAIL_STATE")
+ FW:Fire("MAIL_SENT_DONE", s.sent, #s.jobs)
+end
+
+function Mail:StepSend()
+ local s = self.send
+ local now = Now()
+ if not self.open then return self:FinishSend("closed") end
+ if Mail.BoardBusy() then return self:FinishSend("board") end
+ if s.stage == "wait" then
+ if s.ok then
+ s.stage, s.nextAt = "next", now + SEND_GAP
+ elseif s.err then
+ return self:FinishSend("error")
+ elseif now > s.deadline then
+ return self:FinishSend("timeout")
+ end
+ return
+ end
+ if now < s.nextAt then return end
+ if CursorHasItem and CursorHasItem() then return end
+ s.i = s.i + 1
+ local job = s.jobs[s.i]
+ if not job then return self:FinishSend("done") end
+ s.current = job
+ local ok, why = Prepare(job)
+ if not ok then
+ s.failed = s.failed + 1
+ s.nextAt = now + 0.2
+ Trace(string.format("Brief %d (%s) nicht vorbereitet: %s", s.i, JobLabel(job), tostring(why)))
+ if why == "attach" or why == "gone" or why == "pickup" then
+ Say(string.format(FW.L.MAIL_SEND_FAIL_ATTACH, JobLabel(job)))
+ end
+ FW:Fire("MAIL_STATE")
+ return
+ end
+ s.ok, s.err, s.deadline, s.stage = false, false, now + SEND_TIMEOUT, "wait"
+ Trace(string.format("Brief %d von %d an %s: %s (Betreff '%s', Nachnahme %d)", s.i, #s.jobs, job.to, JobLabel(job), tostring(job.subject), job.cod or 0))
+ FW:Fire("MAIL_STATE")
+ local okc = pcall(SendMail, job.to, job.subject or "", job.body or "")
+ if not okc then s.err = true end
+end
+
+-- Fortschritt fuer die Anzeige: i, n, an wen
+function Mail:SendProgress()
+ local s = self.send
+ if not s then return nil end
+ local job = s.current or s.jobs[1]
+ return math.min(s.i, #s.jobs), #s.jobs, job and job.to or "?"
+end
+
+------------------------------------------------------------------------
+-- Briefe zusammenstellen
+------------------------------------------------------------------------
+
+local function Stamp() return date and date("%d.%m. %H:%M") or "" end
+
+-- Eingabe des Senden-Reiters: form = { to, subject, body, picks = { {id,count}... }, money, cod = bool }
+function Mail:BuildCompose(form)
+ local to = Mail.CleanName(form.to)
+ if to == "" then return nil, "to" end
+ if IsSelf(to) then return nil, "self" end
+ local picks = form.picks or {}
+ local money = math.floor(tonumber(form.money) or 0)
+ if #picks == 0 and money <= 0 then return nil, "nothing" end
+ if form.cod and #picks == 0 then return nil, "codItem" end
+ if form.cod and money <= 0 then return nil, "codMoney" end
+ local base = Trim(form.subject)
+ local jobs = {}
+ local n = math.max(#picks, 1)
+ for i = 1, n do
+ local pick = picks[i]
+ local subject = base
+ if subject == "" then
+ local info = pick and Util.GetItem and Util.GetItem(pick.id)
+ subject = (info and info.name) or FW.L.MAIL_SUBJECT_DEFAULT
+ end
+ if n > 1 then subject = string.format(FW.L.MAIL_SUBJECT_ITEM_N, subject, i, n) end
+ local job = { to = to, subject = subject, body = form.body or "", item = pick }
+ if form.cod then
+ job.cod = money
+ elseif i == 1 and money > 0 then
+ job.money = money
+ end
+ jobs[#jobs + 1] = job
+ end
+ if #jobs > MAX_JOBS then return nil, "many" end
+ return jobs
+end
+
+-- Eintrag der Mailingliste -> Briefe. reserved: Items fuer Brett-Auftraege (werden nicht gesendet)
+function Mail:BuildList(entry, reserved)
+ local jobs = {}
+ if not entry or IsSelf(entry.name) then return jobs end
+ reserved = reserved or Mail.Reserved()
+ local stacks = Mail.BagStacks()
+ local picks = {}
+ for _, id in ipairs(entry.ids or {}) do
+ if not reserved[id] then
+ for _, s in ipairs(stacks) do
+ if s.id == id and not s.locked then picks[#picks + 1] = { id = id, count = s.count } end
+ end
+ end
+ end
+ local surplus = 0
+ if entry.gold then
+ local have = (GetMoney and GetMoney()) or 0
+ surplus = have - (entry.keep or 0) - (#picks + 1) * POSTAGE
+ if surplus < 100 then surplus = 0 end
+ end
+ for _, p in ipairs(picks) do jobs[#jobs + 1] = { to = entry.name, item = p } end
+ if surplus > 0 then jobs[#jobs + 1] = { to = entry.name, money = surplus } end
+ local n, stamp = #jobs, Stamp()
+ for i, job in ipairs(jobs) do
+ job.subject = n > 1 and string.format(FW.L.MAIL_SUBJECT_N, stamp, i, n) or string.format(FW.L.MAIL_SUBJECT_STAMP, stamp)
+ job.body = ""
+ end
+ return jobs
+end
+
+function Mail:ListFor(name)
+ for _, e in ipairs(self.db.lists) do
+ if e.name:lower() == (name or ""):lower() then return e end
+ end
+end
+
+function Mail:AddList(name)
+ name = Mail.CleanName(name)
+ if name == "" or name:find("[^%w\128-\255%-]") then return false, "invalid" end
+ if #self.db.lists >= MAX_LISTS then return false, "full" end
+ if self:ListFor(name) then return false, "exists" end
+ self.db.lists[#self.db.lists + 1] = { name = name, ids = {}, auto = false, gold = false, keep = 0 }
+ FW:Fire("MAIL_LISTS_CHANGED")
+ return true
+end
+
+function Mail:RemoveList(entry)
+ for i, e in ipairs(self.db.lists) do
+ if e == entry then table.remove(self.db.lists, i) break end
+ end
+ FW:Fire("MAIL_LISTS_CHANGED")
+end
+
+function Mail:ToggleListItem(entry, id)
+ for i, v in ipairs(entry.ids) do
+ if v == id then table.remove(entry.ids, i) FW:Fire("MAIL_LISTS_CHANGED") return false end
+ end
+ if #entry.ids >= MAX_ITEMS_PER_LIST then return nil end
+ entry.ids[#entry.ids + 1] = id
+ FW:Fire("MAIL_LISTS_CHANGED")
+ return true
+end
+
+function Mail:HasListItem(entry, id)
+ for _, v in ipairs(entry.ids) do
+ if v == id then return true end
+ end
+ return false
+end
+
+-- Eine Liste (entry) oder alle (entry = nil) jetzt senden. auto: nur Listen mit Haken
+function Mail:SendLists(entry, autoOnly)
+ local reserved = Mail.Reserved()
+ local jobs = {}
+ for _, e in ipairs(self.db.lists) do
+ if (entry == nil or entry == e) and (not autoOnly or e.auto) then
+ local part = self:BuildList(e, reserved)
+ if #part == 0 and entry == e then Say(string.format(FW.L.MAIL_SEND_NOTHING_LIST, e.name)) end
+ for _, j in ipairs(part) do jobs[#jobs + 1] = j end
+ end
+ end
+ if #jobs == 0 then return false, "empty" end
+ return self:StartSend(jobs)
+end
+
+-- Automatik: kurz nach dem Oeffnen, aber erst wenn Brett-Auftraege und andere Aktionen fertig sind
+function Mail:ScheduleAuto()
+ local any
+ for _, e in ipairs(self.db.lists) do if e.auto then any = true end end
+ if not any then return end
+ local gen, tries, told = self.gen, 0, false
+ local function Try()
+ if not Mail.open or Mail.gen ~= gen then return end
+ if Mail.busy or Mail.BoardBusy() then
+ tries = tries + 1
+ if Mail.BoardBusy() and not told then
+ told = true
+ Say(FW.L.MAIL_AUTO_WAIT_BOARD)
+ end
+ if tries < 60 then After(1.5, Try) end
+ return
+ end
+ Mail:SendLists(nil, true)
+ end
+ After(AUTO_DELAY, Try)
+end
+
+------------------------------------------------------------------------
+-- Normales Fenster ausblenden (es bleibt technisch offen, damit Brett-Auftraege weiter funktionieren)
+------------------------------------------------------------------------
+
+function Mail.Replacing()
+ return Mail.db and Mail.db.settings.replace and true or false
+end
+
+function Mail.Anchor()
+ local w = Mail.window
+ if Mail.Replacing() and w and w:IsShown() then return w end
+ return nil
+end
+
+function Mail:ApplyReplace()
+ local f = _G.MailFrame
+ if not f then return end
+ if self.Replacing() and not self.uiFailed then
+ if not self.hidden then
+ self.hidden = { alpha = f:GetAlpha(), scale = f:GetScale() }
+ end
+ f:SetAlpha(0)
+ f:SetScale(0.01)
+ elseif self.hidden then
+ f:SetAlpha(self.hidden.alpha or 1)
+ f:SetScale(self.hidden.scale or 1)
+ self.hidden = nil
+ end
+end
+
+function Mail:SetReplace(on)
+ self.db.settings.replace = on and true or false
+ self.uiFailed = nil
+ self:ApplyReplace()
+ FW:Fire("MAIL_REPLACE_CHANGED")
+ if self.open then
+ FW:Fire(on and "MAIL_WINDOW_SHOW" or "MAIL_WINDOW_HIDE")
+ end
+end
+
+------------------------------------------------------------------------
+-- Ereignisse
+------------------------------------------------------------------------
+
+function Mail:OnMailShow()
+ self.open = true
+ self.gen = self.gen + 1
+ self.selected = {}
+ self:ApplyReplace()
+ Trace("Briefkasten geoeffnet")
+ if self.Replacing() then FW:Fire("MAIL_WINDOW_SHOW") end
+ FW:Fire("MAIL_INBOX_CHANGED")
+ self:ScheduleAuto()
+end
+
+function Mail:OnMailClosed()
+ if not self.open then return end
+ self.open = false
+ if self.busy == "inbox" then FinishOpen(self, "closed") end
+ if self.busy == "send" then self:FinishSend("closed") end
+ self.busy = nil
+ FW:Fire("MAIL_WINDOW_HIDE")
+end
+
+function Mail:OnInboxUpdate()
+ if self.run then self.run.updated = true end
+ FW:Fire("MAIL_INBOX_CHANGED")
+end
+
+function Mail:OnSendSuccess()
+ local s = self.send
+ if self.lastTo then
+ self:AddRecipient(self.lastTo)
+ self.lastTo = nil
+ end
+ if s and s.stage == "wait" then
+ s.ok = true
+ s.sent = s.sent + 1
+ FW:Fire("MAIL_STATE")
+ end
+end
+
+function Mail:OnSendFailed()
+ local s = self.send
+ if s and s.stage == "wait" then s.err = true end
+end
+
+function Mail:OnEnable()
+ self.driver = self.driver or CreateFrame("Frame")
+ local acc = 0
+ self.driver:SetScript("OnUpdate", function(_, elapsed)
+ acc = acc + (elapsed or 0)
+ if acc < STEP then return end
+ acc = 0
+ local ok, err = pcall(function()
+ if Mail.busy == "inbox" and Mail.run then Mail:StepOpen()
+ elseif Mail.busy == "send" and Mail.send then Mail:StepSend()
+ else Mail.driver:Hide() end
+ end)
+ if not ok then
+ Trace("Fehler im Takt: " .. tostring(err))
+ Mail.run, Mail.send, Mail.busy = nil, nil, nil
+ Mail.driver:Hide()
+ FW:Fire("MAIL_STATE")
+ end
+ end)
+ self.driver:Hide()
+ FW:RegisterEvent("MAIL_SHOW", function() pcall(Mail.OnMailShow, Mail) end, self)
+ FW:RegisterEvent("MAIL_CLOSED", function() pcall(Mail.OnMailClosed, Mail) end, self)
+ FW:RegisterEvent("MAIL_INBOX_UPDATE", function() pcall(Mail.OnInboxUpdate, Mail) end, self)
+ FW:RegisterEvent("MAIL_SEND_SUCCESS", function() pcall(Mail.OnSendSuccess, Mail) end, self)
+ FW:RegisterEvent("MAIL_FAILED", function() pcall(Mail.OnSendFailed, Mail) end, self)
+ if not self.hooked then
+ self.hooked = true
+ -- Empfaenger merken, egal wer sendet (auch das normale Fenster und Brett-Auftraege)
+ if hooksecurefunc and _G.SendMail then
+ pcall(hooksecurefunc, "SendMail", function(to)
+ if Mail.db then Mail.lastTo = to end
+ end)
+ end
+ if _G.MailFrame and MailFrame.HookScript then
+ pcall(MailFrame.HookScript, MailFrame, "OnShow", function() pcall(Mail.ApplyReplace, Mail) end)
+ -- das Ereignis MAIL_CLOSED kommt nicht immer; das Verschwinden des Fensters ist sicher
+ pcall(MailFrame.HookScript, MailFrame, "OnHide", function() pcall(Mail.OnMailClosed, Mail) end)
+ end
+ end
+ -- Briefkasten schon offen (z. B. nach /reload)?
+ if _G.MailFrame and MailFrame.IsShown and MailFrame:IsShown() then
+ After(0.5, function() pcall(Mail.OnMailShow, Mail) end)
+ end
+end
+
+function Mail:OnDisable()
+ for _, ev in ipairs({ "MAIL_SHOW", "MAIL_CLOSED", "MAIL_INBOX_UPDATE", "MAIL_SEND_SUCCESS", "MAIL_FAILED" }) do
+ FW:UnregisterEvent(ev, self)
+ end
+ if self.driver then self.driver:Hide() end
+ self.run, self.send, self.busy = nil, nil, nil
+ if self.hidden then
+ local f = _G.MailFrame
+ if f then f:SetAlpha(self.hidden.alpha or 1) f:SetScale(self.hidden.scale or 1) end
+ self.hidden = nil
+ end
+end
diff --git a/MailUI.lua b/MailUI.lua
new file mode 100644
index 0000000..f9d756c
--- /dev/null
+++ b/MailUI.lua
@@ -0,0 +1,845 @@
+-- Freewood-OctoWoW-Mail: eigenes Briefkasten-Fenster (Freewood-Design, verschiebbar).
+-- Es geht nur auf, wenn im Spiel ein Briefkasten geoeffnet wird, und nur, wenn es in den Einstellungen so gewaehlt ist.
+-- Reiter: Posteingang | Senden | Mailinglisten.
+
+local ADDON, NS = ...
+
+local FW = _G.Freewood
+local Mail = NS.Mail
+if not FW or not Mail then
+ return
+end
+
+local UI = FW.UI
+local Util = FW.Util
+
+local W, H = 660, 560
+local ROW_H, ROW_GAP = 40, 4
+local OWNER = {}
+
+local window, content, footer, tabs
+local current = "inbox"
+local panes = {}
+local picks = {} -- Senden-Reiter: gewaehlte Staepel { id, count, key }
+local picker
+
+local function T(key) return FW.L[key] or key end
+
+------------------------------------------------------------------------
+-- Bausteine
+------------------------------------------------------------------------
+
+local function ColoredName(name, quality, id)
+ return "|cff" .. UI.RarityHex(quality) .. (name or ("#" .. tostring(id))) .. "|r"
+end
+
+local function MakeEdit(parent, width, hintText, maxLetters, numeric)
+ local box = UI.CreatePanel(parent, "bgDeep", "bronze")
+ box:SetSize(width, 26)
+ local edit = CreateFrame("EditBox", nil, box)
+ edit:SetAutoFocus(false)
+ edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2)
+ edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2)
+ edit:SetMaxLetters(maxLetters or 100)
+ if numeric and edit.SetNumeric then pcall(edit.SetNumeric, edit, true) end
+ if not edit:SetFont(UI.GetFontPath("body"), 12, "") then
+ edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
+ end
+ edit:SetTextColor(UI.Color("text"))
+ edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
+ edit:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
+ if hintText then
+ box.hint = UI.CreateLabel(box, hintText, { size = 12, color = "textDim" })
+ box.hint:SetPoint("LEFT", box, "LEFT", 10, 0)
+ end
+ edit:SetScript("OnTextChanged", function(self)
+ if box.hint then
+ if self:GetText() == "" then box.hint:Show() else box.hint:Hide() end
+ end
+ if box.onChange and not box.loading then box.onChange(self:GetText()) end
+ end)
+ box.edit = edit
+ function box:Set(text)
+ self.loading = true
+ edit:SetText(text or "")
+ self.loading = nil
+ if self.hint then
+ if (text or "") == "" then self.hint:Show() else self.hint:Hide() end
+ end
+ end
+ return box, edit
+end
+
+local function Tooltip(frame, itemID, inboxIndex)
+ frame:EnableMouse(true)
+ frame:SetScript("OnEnter", function(self)
+ if not GameTooltip then return end
+ local idx = self.inboxIndex
+ local id = self.itemID
+ if idx and GameTooltip.SetInboxItem then
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ pcall(GameTooltip.SetInboxItem, GameTooltip, idx)
+ GameTooltip:Show()
+ elseif id then
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ pcall(GameTooltip.SetItemByID, GameTooltip, id)
+ GameTooltip:Show()
+ end
+ end)
+ frame:SetScript("OnLeave", function()
+ if GameTooltip then GameTooltip:Hide() end
+ end)
+end
+
+-- Zeile mit Symbol und Namen
+local function ItemRow(parent, h)
+ local row = UI.CreatePanel(parent, "card", "bronze")
+ row:SetHeight(h or 32)
+ row.icon = row:CreateTexture(nil, "ARTWORK")
+ row.icon:SetSize(24, 24)
+ row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ row.name = UI.CreateLabel(row, "", { size = 12 })
+ row.name:SetWordWrap(false)
+ Tooltip(row)
+ return row
+end
+
+local function FillItem(row, id, count)
+ row.itemID = id
+ local suffix = (count and count > 1) and (" x" .. count) or ""
+ row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
+ row.name:SetText(ColoredName(nil, 1, id) .. suffix)
+ Util.RequestItem(id, function(info)
+ if row.itemID ~= id or not info then return end
+ row.icon:SetTexture(info.icon)
+ row.name:SetText(ColoredName(info.name, info.quality, id) .. suffix)
+ end)
+end
+
+local function Heading(parent, text, x, y)
+ local h = UI.CreateLabel(parent, text, { font = "heading", size = 13, color = "goldLight" })
+ h:SetPoint("TOPLEFT", parent, "TOPLEFT", x or 0, y or 0)
+ return h
+end
+
+------------------------------------------------------------------------
+-- Auswahlfenster fuer Items
+------------------------------------------------------------------------
+
+local function BuildPicker()
+ local p = UI.CreatePanel(window, "bgDeep", "gold")
+ p:SetSize(460, 440)
+ p:SetPoint("CENTER", window, "CENTER", 0, 0)
+ p:SetFrameStrata("DIALOG")
+ p:EnableMouse(true)
+ p.title = UI.CreateLabel(p, "", { font = "heading", size = 15, color = "goldLight" })
+ p.title:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -14)
+ p.hint = UI.CreateLabel(p, "", { size = 11, color = "textDim", justifyV = "TOP" })
+ p.hint:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -38)
+ p.hint:SetPoint("TOPRIGHT", p, "TOPRIGHT", -16, -38)
+ p.hint:SetHeight(28)
+ p.hint:SetWordWrap(true)
+ p.scroll = UI.CreateScrollFrame(p)
+ p.scroll:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -72)
+ p.scroll:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", -16, 52)
+ p.empty = UI.CreateLabel(p.scroll.child, T("MAIL_PICK_EMPTY"), { size = 12, color = "textDim" })
+ p.empty:SetPoint("TOPLEFT", p.scroll.child, "TOPLEFT", 4, -6)
+ p.done = UI.CreateButton(p, T("MAIL_PICK_DONE"), 140, 28, function() p:Hide() end, "primary")
+ p.done:SetPoint("BOTTOM", p, "BOTTOM", 0, 14)
+ p.rows = {}
+ p:Hide()
+
+ local function Row(i)
+ local row = p.rows[i]
+ if row then return row end
+ row = ItemRow(p.scroll.child, 32)
+ row.cb = UI.CreateCheckbox(row, "", function() return row.checked end, function(v)
+ if row.onToggle then row.onToggle(v) end
+ end)
+ row.cb:SetPoint("LEFT", row, "LEFT", 6, 0)
+ row.cb:SetWidth(22)
+ row.icon:SetPoint("LEFT", row, "LEFT", 34, 0)
+ row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
+ row.name:SetPoint("RIGHT", row, "RIGHT", -8, 0)
+ p.rows[i] = row
+ return row
+ end
+
+ function p:Fill()
+ local data = {}
+ local stacks = Mail.BagStacks()
+ if self.mode == "list" then
+ local agg, order = {}, {}
+ for _, s in ipairs(stacks) do
+ local a = agg[s.id]
+ if not a then
+ a = { id = s.id, count = 0 }
+ agg[s.id] = a
+ order[#order + 1] = a
+ end
+ a.count = a.count + s.count
+ end
+ table.sort(order, function(a, b) return a.id < b.id end)
+ for _, a in ipairs(order) do data[#data + 1] = a end
+ local extraStart = #data + 1
+ for _, id in ipairs(self.entry.ids) do
+ if not agg[id] then data[#data + 1] = { id = id, count = 0, extra = true } end
+ end
+ self.extraStart = extraStart
+ else
+ local seen = {}
+ for _, s in ipairs(stacks) do
+ if not s.locked then
+ local base = s.id .. ":" .. s.count
+ seen[base] = (seen[base] or 0) + 1
+ data[#data + 1] = { id = s.id, count = s.count, key = base .. ":" .. seen[base] }
+ end
+ end
+ end
+ for i, d in ipairs(data) do local d = d
+ local row = Row(i)
+ row:ClearAllPoints()
+ row:SetPoint("TOPLEFT", p.scroll.child, "TOPLEFT", 0, -((i - 1) * 36))
+ row:SetPoint("TOPRIGHT", p.scroll.child, "TOPRIGHT", 0, -((i - 1) * 36))
+ FillItem(row, d.id, d.count)
+ if d.extra then row.name:SetText(row.name:GetText() .. " |cff8a8f9a(" .. T("MAIL_PICK_EXTRA") .. ")|r") end
+ if self.mode == "list" then
+ row.checked = Mail:HasListItem(self.entry, d.id)
+ row.onToggle = function()
+ local r = Mail:ToggleListItem(self.entry, d.id)
+ row.checked = (r == true) or (r == nil and false)
+ row.cb:Refresh()
+ if self.onChange then self.onChange() end
+ end
+ else
+ local function Index()
+ for k, pk in ipairs(picks) do if pk.key == d.key then return k end end
+ end
+ row.checked = Index() and true or false
+ row.onToggle = function()
+ local k = Index()
+ if k then
+ table.remove(picks, k)
+ row.checked = false
+ else
+ picks[#picks + 1] = { id = d.id, count = d.count, key = d.key }
+ row.checked = true
+ end
+ row.cb:Refresh()
+ if self.onChange then self.onChange() end
+ end
+ end
+ row.cb:Refresh()
+ row:Show()
+ end
+ for i = #data + 1, #p.rows do p.rows[i]:Hide() end
+ self.empty:SetShown(#data == 0)
+ self.scroll:SetContentHeight(#data * 36 + 4)
+ end
+
+ return p
+end
+
+-- mode "list": entry = Eintrag der Mailingliste; mode "send": Auswahl des Senden-Reiters
+local function OpenPicker(mode, entry, onChange)
+ picker = picker or BuildPicker()
+ picker.mode, picker.entry, picker.onChange = mode, entry, onChange
+ if mode == "list" then
+ picker.title:SetText(string.format(T("MAIL_PICK_LIST_TITLE"), entry.name))
+ picker.hint:SetText(T("MAIL_PICK_HINT_LIST"))
+ else
+ picker.title:SetText(T("MAIL_PICK_TITLE"))
+ picker.hint:SetText(T("MAIL_PICK_HINT_SEND"))
+ end
+ picker:Show()
+ picker:Raise()
+ picker:Fill()
+end
+
+------------------------------------------------------------------------
+-- Reiter: Posteingang
+------------------------------------------------------------------------
+
+local function BuildInbox(parent)
+ local f = CreateFrame("Frame", nil, parent)
+ local rows = {}
+
+ f.info = UI.CreateLabel(f, "", { size = 12, color = "textDim" })
+ f.info:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -6)
+
+ f.none = UI.CreateButton(f, T("MAIL_SELECT_NONE"), 130, 24, function() Mail:SelectAll(false) end, "default")
+ f.none:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
+ f.all = UI.CreateButton(f, T("MAIL_SELECT_ALL"), 130, 24, function() Mail:SelectAll(true) end, "default")
+ f.all:SetPoint("RIGHT", f.none, "LEFT", -6, 0)
+
+ f.open = UI.CreateButton(f, T("MAIL_OPEN_ALL"), 280, 30, function()
+ Mail:StartOpen(Mail:SelectedCount() > 0)
+ end, "primary")
+ f.open:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -32)
+ f.stop = UI.CreateButton(f, T("MAIL_STOP"), 110, 30, function() Mail:Stop() end, "danger")
+ f.stop:SetPoint("LEFT", f.open, "RIGHT", 8, 0)
+
+ f.note = UI.CreateLabel(f, T("MAIL_NOTE_SAFE"), { size = 11, color = "textDim", justifyV = "TOP" })
+ f.note:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)
+ f.note:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
+ f.note:SetHeight(30)
+ f.note:SetWordWrap(true)
+
+ f.scroll = UI.CreateScrollFrame(f)
+ f.scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -70)
+ f.scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 36)
+ f.empty = UI.CreateLabel(f.scroll.child, T("MAIL_INBOX_EMPTY"), { size = 12, color = "textDim" })
+ f.empty:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 4, -6)
+
+ local function Row(i)
+ local row = rows[i]
+ if row then return row end
+ row = UI.CreatePanel(f.scroll.child, "card", "bronze")
+ row:SetHeight(ROW_H)
+ row.cb = UI.CreateCheckbox(row, "", function() return row.mail and Mail.selected[row.mail.key] end, function(v)
+ if row.mail then Mail:ToggleSelect(row.mail.key, v) end
+ end)
+ row.cb:SetPoint("LEFT", row, "LEFT", 8, 0)
+ row.cb:SetWidth(22)
+ row.icon = row:CreateTexture(nil, "ARTWORK")
+ row.icon:SetSize(28, 28)
+ row.icon:SetPoint("LEFT", row, "LEFT", 38, 0)
+ row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ row.l1 = UI.CreateLabel(row, "", { size = 12 })
+ row.l1:SetPoint("TOPLEFT", row, "TOPLEFT", 76, -6)
+ row.l1:SetPoint("RIGHT", row, "RIGHT", -60, 0)
+ row.l1:SetWordWrap(false)
+ row.l2 = UI.CreateLabel(row, "", { size = 11, color = "textDim" })
+ row.l2:SetPoint("TOPLEFT", row, "TOPLEFT", 76, -22)
+ row.l2:SetPoint("RIGHT", row, "RIGHT", -60, 0)
+ row.l2:SetWordWrap(false)
+ row.days = UI.CreateLabel(row, "", { size = 11, color = "textDim", justify = "RIGHT" })
+ row.days:SetPoint("RIGHT", row, "RIGHT", -10, 0)
+ Tooltip(row)
+ rows[i] = row
+ return row
+ end
+
+ function f.Refresh()
+ local list = Mail.ReadInbox()
+ local busy = Mail.busy
+ local takeable = 0
+ for i, m in ipairs(list) do local m = m
+ local row = Row(i)
+ row.mail = m
+ row.inboxIndex = m.hasItem and m.i or nil
+ row:ClearAllPoints()
+ row:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 0, -((i - 1) * (ROW_H + ROW_GAP)))
+ row:SetPoint("TOPRIGHT", f.scroll.child, "TOPRIGHT", 0, -((i - 1) * (ROW_H + ROW_GAP)))
+ if m.itemTex then
+ row.icon:SetTexture(m.itemTex)
+ elseif m.money > 0 then
+ row.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
+ else
+ row.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01")
+ end
+ row.l1:SetText(m.subject ~= "" and m.subject or m.sender)
+ local parts = {}
+ if m.hasItem then parts[#parts + 1] = (m.itemName or "?") .. ((m.itemCount or 1) > 1 and (" x" .. m.itemCount) or "") end
+ if m.money > 0 then parts[#parts + 1] = string.format(T("MAIL_ROW_GOLD"), Mail.Money(m.money)) end
+ local text
+ if m.cod > 0 then
+ text = "|cffff7060" .. string.format(T("MAIL_ROW_COD"), Mail.Money(m.cod)) .. "|r"
+ elseif #parts == 0 then
+ text = T("MAIL_ROW_LETTER")
+ else
+ text = table.concat(parts, " + ")
+ end
+ row.l2:SetText(string.format(T("MAIL_ROW_FROM"), m.sender) .. " " .. text)
+ row.days:SetText(m.days and string.format(T("MAIL_ROW_DAYS"), math.floor(m.days)) or "")
+ row.cb:SetShown(m.take)
+ row:SetAlpha(m.take and 1 or 0.55)
+ row.cb:Refresh()
+ if m.take then takeable = takeable + 1 end
+ row:Show()
+ end
+ for i = #list + 1, #rows do rows[i].mail = nil rows[i]:Hide() end
+ f.empty:SetShown(#list == 0)
+ f.scroll:SetContentHeight(#list * (ROW_H + ROW_GAP) + 4)
+ f.info:SetText(string.format(T("MAIL_INBOX_COUNT"), #list))
+
+ local sel = Mail:SelectedCount()
+ f.open:SetLabel(sel > 0 and string.format(T("MAIL_OPEN_SELECTED"), sel) or T("MAIL_OPEN_ALL"))
+ f.open:SetButtonEnabled(Mail.open and not busy and (sel > 0 or takeable > 0))
+ f.stop:SetShown(busy ~= nil)
+ f.all:SetButtonEnabled(not busy and takeable > 0)
+ f.none:SetButtonEnabled(not busy and sel > 0)
+ end
+
+ return f
+end
+
+------------------------------------------------------------------------
+-- Reiter: Senden
+------------------------------------------------------------------------
+
+local function BuildSend(parent)
+ local f = CreateFrame("Frame", nil, parent)
+ local rows = {}
+ local cod = false
+
+ Heading(f, T("MAIL_TO"), 0, 0)
+ local toBox, toEdit = MakeEdit(f, 300, nil, 24)
+ toBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -20)
+ local dd
+ dd = UI.CreateDropdown(f, 170, 26, {
+ allText = T("MAIL_TO_RECENT"),
+ getOptions = function()
+ local o = {}
+ for _, n in ipairs(Mail:Recipients()) do o[#o + 1] = { value = n, text = n } end
+ return o
+ end,
+ onChange = function(v)
+ if v then toBox:Set(v) end
+ dd:SetValue(nil)
+ end,
+ })
+ dd:SetPoint("LEFT", toBox, "RIGHT", 8, 0)
+
+ Heading(f, T("MAIL_SUBJECT"), 0, -54)
+ local subjBox = MakeEdit(f, 628, nil, 64)
+ subjBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -74)
+
+ Heading(f, T("MAIL_BODY"), 0, -108)
+ local body = UI.CreateTextArea(f, 628, 64)
+ body:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -128)
+
+ -- Items
+ Heading(f, T("MAIL_ITEMS"), 0, -202)
+ local addBtn = UI.CreateButton(f, T("MAIL_ITEMS_ADD"), 170, 24, nil, "default")
+ addBtn:SetPoint("TOPLEFT", f, "TOPLEFT", 120, -198)
+ local itemScroll = UI.CreateScrollFrame(f)
+ itemScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -228)
+ itemScroll:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -228)
+ itemScroll:SetHeight(104)
+ local itemsEmpty = UI.CreateLabel(itemScroll.child, T("MAIL_ITEMS_NONE"), { size = 12, color = "textDim" })
+ itemsEmpty:SetPoint("TOPLEFT", itemScroll.child, "TOPLEFT", 4, -6)
+
+ -- Gold und Nachnahme
+ local goldBox = MakeEdit(f, 90, T("MAIL_GOLD"), 7, true)
+ goldBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -344)
+ local silverBox = MakeEdit(f, 70, T("MAIL_SILVER"), 2, true)
+ silverBox:SetPoint("LEFT", goldBox, "RIGHT", 8, 0)
+ local copperBox = MakeEdit(f, 70, T("MAIL_COPPER"), 2, true)
+ copperBox:SetPoint("LEFT", silverBox, "RIGHT", 8, 0)
+ local codBox = UI.CreateCheckbox(f, T("MAIL_COD"), function() return cod end, function(v) cod = v and true or false end)
+ codBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -376)
+ codBox:SetWidth(620)
+
+ local status = UI.CreateLabel(f, "", { size = 12, color = "textDim", justify = "LEFT" })
+ status:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 6)
+ status:SetPoint("RIGHT", f, "RIGHT", -160, 0)
+ status:SetWordWrap(false)
+ local send = UI.CreateButton(f, T("MAIL_SEND"), 150, 30, nil, "primary")
+ send:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
+
+ local function Copper()
+ return (tonumber(goldBox.edit:GetText()) or 0) * 10000 + (tonumber(silverBox.edit:GetText()) or 0) * 100 + (tonumber(copperBox.edit:GetText()) or 0)
+ end
+
+ local function Form()
+ return { to = toEdit:GetText(), subject = subjBox.edit:GetText(), body = body.edit:GetText(), picks = picks, money = Copper(), cod = cod }
+ end
+
+ local function SetStatus(text, colorKey)
+ status:SetText(text or "")
+ status:SetTextColor(UI.Color(colorKey or "textDim"))
+ end
+
+ function f.Refresh()
+ for i, pk in ipairs(picks) do local pk, i = pk, i
+ local row = rows[i]
+ if not row then
+ row = ItemRow(itemScroll.child, 30)
+ row.icon:SetPoint("LEFT", row, "LEFT", 6, 0)
+ row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
+ row.name:SetPoint("RIGHT", row, "RIGHT", -40, 0)
+ row.remove = UI.CreateButton(row, "X", 26, 22, nil, "danger")
+ row.remove:SetPoint("RIGHT", row, "RIGHT", -4, 0)
+ rows[i] = row
+ end
+ row:ClearAllPoints()
+ row:SetPoint("TOPLEFT", itemScroll.child, "TOPLEFT", 0, -((i - 1) * 34))
+ row:SetPoint("TOPRIGHT", itemScroll.child, "TOPRIGHT", 0, -((i - 1) * 34))
+ FillItem(row, pk.id, pk.count)
+ row.remove:SetScript("OnClick", function()
+ table.remove(picks, i)
+ f.Refresh()
+ end)
+ row:Show()
+ end
+ for i = #picks + 1, #rows do rows[i]:Hide() end
+ itemsEmpty:SetShown(#picks == 0)
+ itemScroll:SetContentHeight(#picks * 34 + 4)
+ codBox:Refresh()
+
+ local busy = Mail.busy
+ local boardBusy = Mail.BoardBusy()
+ local jobs, err = Mail:BuildCompose(Form())
+ if boardBusy then
+ SetStatus(T("MAIL_BOARD_BUSY"), "goldLight")
+ elseif busy then
+ SetStatus(T("MAIL_BUSY"), "textDim")
+ elseif jobs then
+ SetStatus(#jobs == 1 and T("MAIL_SEND_COUNT1") or string.format(T("MAIL_SEND_COUNT"), #jobs), "textDim")
+ elseif err == "to" then SetStatus(T("MAIL_SEND_NO_TO"), "textDim")
+ elseif err == "self" then SetStatus(T("MAIL_SEND_SELF"), "danger")
+ elseif err == "codItem" then SetStatus(T("MAIL_SEND_COD_NEEDS_ITEM"), "danger")
+ else SetStatus(T("MAIL_SEND_NOTHING"), "textDim") end
+ send:SetButtonEnabled(jobs ~= nil and Mail.open and not busy and not boardBusy)
+ end
+
+ for _, b in ipairs({ goldBox, silverBox, copperBox, subjBox }) do b.onChange = function() if f:IsShown() then f.Refresh() end end end
+ toBox.onChange = function() if f:IsShown() then f.Refresh() end end
+ addBtn:SetScript("OnClick", function() OpenPicker("send", nil, f.Refresh) end)
+ send:SetScript("OnClick", function()
+ local jobs = Mail:BuildCompose(Form())
+ if not jobs then return f.Refresh() end
+ local ok = Mail:StartSend(jobs)
+ if ok then f.pendingClear = #jobs end
+ f.Refresh()
+ end)
+ FW:On("MAIL_SENT_DONE", function(_, sent, total)
+ if f.pendingClear and sent >= f.pendingClear then
+ for i = #picks, 1, -1 do table.remove(picks, i) end
+ subjBox:Set("")
+ body.edit:SetText("")
+ goldBox:Set("") silverBox:Set("") copperBox:Set("")
+ cod = false
+ end
+ f.pendingClear = nil
+ if f:IsShown() then f.Refresh() end
+ end, OWNER)
+ FW:On("MAIL_RECIPIENTS_CHANGED", function() end, OWNER)
+
+ -- Brett-Auftraege melden sich nicht bei uns; das Fenster fragt deshalb im Takt nach
+ local acc = 0
+ f:SetScript("OnUpdate", function(_, elapsed)
+ acc = acc + (elapsed or 0)
+ if acc > 0.5 then
+ acc = 0
+ f.Refresh()
+ end
+ end)
+
+ return f
+end
+
+------------------------------------------------------------------------
+-- Reiter: Mailinglisten
+------------------------------------------------------------------------
+
+local LIST_ROW_H = 96
+
+local function BuildLists(parent)
+ local f = CreateFrame("Frame", nil, parent)
+ local rows = {}
+
+ local nameBox, nameEdit = MakeEdit(f, 220, T("MAIL_LISTS_NAME_BOX"), 24)
+ nameBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
+ local status = UI.CreateLabel(f, "", { size = 11, color = "textDim" })
+ local function Add()
+ local ok, why = Mail:AddList(nameEdit:GetText())
+ if ok then
+ nameBox:Set("")
+ nameEdit:ClearFocus()
+ status:SetText("")
+ else
+ status:SetText(why == "exists" and "Name exists." or (why == "full" and "List is full." or "Invalid name."))
+ end
+ end
+ local addBtn = UI.CreateButton(f, T("MAIL_LISTS_ADD"), 110, 26, Add, "primary")
+ addBtn:SetPoint("LEFT", nameBox, "RIGHT", 8, 0)
+ nameEdit:SetScript("OnEnterPressed", Add)
+ status:SetPoint("LEFT", addBtn, "RIGHT", 10, 0)
+ local allBtn = UI.CreateButton(f, T("MAIL_LISTS_ALL_NOW"), 190, 26, function() Mail:SendLists(nil, false) end, "default")
+ allBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
+
+ local hint = UI.CreateLabel(f, T("MAIL_LISTS_HINT"), { size = 11, color = "textDim", justifyV = "TOP" })
+ hint:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -34)
+ hint:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -34)
+ hint:SetHeight(28)
+ hint:SetWordWrap(true)
+
+ local reserved = UI.CreateLabel(f, "", { size = 11, color = "goldLight" })
+ reserved:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 2)
+
+ local scroll = UI.CreateScrollFrame(f)
+ scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -66)
+ scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 24)
+ local empty = UI.CreateLabel(scroll.child, T("MAIL_LISTS_EMPTY"), { size = 12, color = "textDim" })
+ empty:SetPoint("TOPLEFT", scroll.child, "TOPLEFT", 4, -6)
+
+ local function Row(i)
+ local row = rows[i]
+ if row then return row end
+ row = UI.CreatePanel(scroll.child, "card", "bronze")
+ row:SetHeight(LIST_ROW_H)
+ row.name = UI.CreateLabel(row, "", { font = "heading", size = 14, color = "goldLight" })
+ row.name:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -12)
+ row.items = UI.CreateButton(row, "", 130, 24, function()
+ if row.entry then OpenPicker("list", row.entry, function() f.Refresh() end) end
+ end, "default")
+ row.items:SetPoint("TOPLEFT", row, "TOPLEFT", 190, -8)
+ row.now = UI.CreateButton(row, T("MAIL_LISTS_NOW"), 120, 24, function()
+ if row.entry then Mail:SendLists(row.entry, false) end
+ end, "default")
+ row.now:SetPoint("LEFT", row.items, "RIGHT", 8, 0)
+ row.remove = UI.CreateButton(row, "X", 26, 24, function()
+ if row.entry then Mail:RemoveList(row.entry) end
+ end, "danger")
+ row.remove:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -8)
+ UI.AttachTooltip(row.remove, function() return T("MAIL_LISTS_REMOVE") end, "")
+ row.selfNote = UI.CreateLabel(row, T("MAIL_LISTS_SELF"), { size = 11, color = "textDim" })
+ row.selfNote:SetPoint("LEFT", row.items, "RIGHT", 8, 0)
+
+ row.auto = UI.CreateCheckbox(row, T("MAIL_LISTS_AUTO"), function() return row.entry and row.entry.auto end, function(v)
+ if row.entry then row.entry.auto = v and true or false end
+ end)
+ row.auto:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -40)
+ row.auto:SetWidth(420)
+ row.gold = UI.CreateCheckbox(row, T("MAIL_LISTS_GOLD"), function() return row.entry and row.entry.gold end, function(v)
+ if row.entry then row.entry.gold = v and true or false end
+ end)
+ row.gold:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -66)
+ row.gold:SetWidth(170)
+ row.keepBox = MakeEdit(row, 80, nil, 7, true)
+ row.keepBox:SetPoint("TOPLEFT", row, "TOPLEFT", 186, -64)
+ row.keepBox.onChange = function(text)
+ if row.entry then row.entry.keep = math.max(0, math.floor(tonumber(text) or 0)) * 10000 end
+ end
+ row.keepLabel = UI.CreateLabel(row, T("MAIL_LISTS_GOLD_G"), { size = 12, color = "textDim" })
+ row.keepLabel:SetPoint("LEFT", row.keepBox, "RIGHT", 8, 0)
+ rows[i] = row
+ return row
+ end
+
+ function f.Refresh()
+ local lists = Mail.db.lists
+ for i, e in ipairs(lists) do
+ local row = Row(i)
+ row.entry = e
+ row:ClearAllPoints()
+ row:SetPoint("TOPLEFT", scroll.child, "TOPLEFT", 0, -((i - 1) * (LIST_ROW_H + ROW_GAP)))
+ row:SetPoint("TOPRIGHT", scroll.child, "TOPRIGHT", 0, -((i - 1) * (LIST_ROW_H + ROW_GAP)))
+ row.name:SetText(e.name)
+ row.items:SetLabel(string.format(T("MAIL_LISTS_ITEMS"), #e.ids))
+ local me = Mail.IsSelf(e.name)
+ row.now:SetShown(not me)
+ row.selfNote:SetShown(me and true or false)
+ row.now:SetButtonEnabled(Mail.open and not Mail.busy and not Mail.BoardBusy())
+ row.auto:Refresh()
+ row.gold:Refresh()
+ row.keepBox:Set(tostring(math.floor((e.keep or 0) / 10000)))
+ row:Show()
+ end
+ for i = #lists + 1, #rows do rows[i].entry = nil rows[i]:Hide() end
+ empty:SetShown(#lists == 0)
+ scroll:SetContentHeight(#lists * (LIST_ROW_H + ROW_GAP) + 4)
+ allBtn:SetButtonEnabled(#lists > 0 and Mail.open and not Mail.busy and not Mail.BoardBusy())
+ local _, n = Mail.Reserved()
+ reserved:SetText(n > 0 and string.format(T("MAIL_LISTS_RESERVED"), n) or "")
+ end
+
+ return f
+end
+
+------------------------------------------------------------------------
+-- Fenster
+------------------------------------------------------------------------
+
+local TAB_DEFS = {
+ { id = "inbox", label = "MAIL_TAB_INBOX", build = BuildInbox },
+ { id = "send", label = "MAIL_TAB_SEND", build = BuildSend },
+ { id = "lists", label = "MAIL_TAB_LISTS", build = BuildLists },
+}
+
+local function SelectTab(id)
+ current = id
+ for _, def in ipairs(TAB_DEFS) do
+ local pane = panes[def.id]
+ if pane then
+ if def.id == id then pane:Show() pane.Refresh() else pane:Hide() end
+ end
+ if tabs[def.id] then tabs[def.id]:SetActive(def.id == id) end
+ end
+end
+
+local function UpdateFooter()
+ if not footer then return end
+ local text = ""
+ if Mail.busy == "inbox" and Mail.run then
+ text = string.format(T("MAIL_RUN_PROGRESS"), Mail.run.items, Mail.run.money)
+ elseif Mail.busy == "send" then
+ local i, n, to = Mail:SendProgress()
+ if i then text = string.format(T("MAIL_SEND_PROGRESS"), math.max(i, 1), n, to) end
+ end
+ footer:SetText(text)
+end
+
+local function SavePosition()
+ local left, top = window:GetLeft(), window:GetTop()
+ if left and top then Mail.db.settings.pos = { x = left, y = top } end
+end
+
+local function BuildWindow()
+ local f = UI.CreatePanel(UIParent, "bgDeep", "gold")
+ f:SetSize(W, H)
+ f:SetFrameStrata("HIGH")
+ f:SetClampedToScreen(true)
+ f:EnableMouse(true)
+ f:SetMovable(true)
+ f:RegisterForDrag("LeftButton")
+ f:SetScript("OnDragStart", function(self) self:StartMoving() end)
+ f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() SavePosition() end)
+ local pos = Mail.db.settings.pos
+ f:ClearAllPoints()
+ if pos and pos.x and pos.y then
+ f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos.x, pos.y)
+ else
+ f:SetPoint("CENTER", UIParent, "CENTER", -120, 40)
+ end
+ window = f
+ Mail.window = f
+
+ local title = UI.CreateLabel(f, T("MAIL_WINDOW_TITLE"), { font = "heading", size = 16, color = "goldLight" })
+ title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -12)
+ local close = UI.CreateButton(f, "X", 26, 24, function()
+ if CloseMail then pcall(CloseMail) end
+ if _G.MailFrame and HideUIPanel and MailFrame:IsShown() then pcall(HideUIPanel, MailFrame) end
+ f:Hide()
+ end, "danger")
+ close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -10)
+ UI.AttachTooltip(close, function() return T("MAIL_CLOSE") end, "")
+ local div = UI.CreateDivider(f)
+ div:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -40)
+ div:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -40)
+
+ tabs = {}
+ local prev
+ for _, def in ipairs(TAB_DEFS) do local def = def
+ local b = UI.CreateTabButton(f, T(def.label))
+ if prev then b:SetPoint("LEFT", prev, "RIGHT", 4, 0) else b:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -44) end
+ b:SetScript("OnClick", function() SelectTab(def.id) end)
+ tabs[def.id] = b
+ prev = b
+ end
+
+ content = CreateFrame("Frame", nil, f)
+ content:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -86)
+ content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 32)
+ for _, def in ipairs(TAB_DEFS) do
+ local pane = def.build(content)
+ pane:SetAllPoints(content)
+ pane:Hide()
+ panes[def.id] = pane
+ end
+
+ footer = UI.CreateLabel(f, "", { size = 11, color = "textDim" })
+ footer:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 10)
+ footer:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 10)
+ footer:SetWordWrap(false)
+
+ f:Hide()
+ return f
+end
+
+local function RefreshCurrent()
+ local pane = panes[current]
+ if window and window:IsShown() and pane and pane.Refresh then pane.Refresh() end
+ UpdateFooter()
+end
+
+local function ShowWindow()
+ local ok, err = pcall(function()
+ if not window then BuildWindow() end
+ window:Show()
+ SelectTab(current)
+ UpdateFooter()
+ end)
+ if not ok then
+ -- Fehler: das normale Fenster zurueckgeben, damit der Briefkasten benutzbar bleibt
+ Mail.uiFailed = true
+ Mail:ApplyReplace()
+ Mail.Trace("Fenster-Fehler: " .. tostring(err))
+ if FW.Print then FW:Print("Mail: " .. tostring(err)) end
+ end
+end
+
+local function HideWindow()
+ if window then
+ if picker then picker:Hide() end
+ window:Hide()
+ end
+end
+
+FW:On("MAIL_WINDOW_SHOW", function() ShowWindow() end, OWNER)
+FW:On("MAIL_WINDOW_HIDE", function() HideWindow() end, OWNER)
+FW:On("MAIL_STATE", function() RefreshCurrent() end, OWNER)
+FW:On("MAIL_INBOX_CHANGED", function() RefreshCurrent() end, OWNER)
+FW:On("MAIL_SELECTION_CHANGED", function() RefreshCurrent() end, OWNER)
+FW:On("MAIL_LISTS_CHANGED", function() RefreshCurrent() if picker and picker:IsShown() then picker:Fill() end end, OWNER)
+
+------------------------------------------------------------------------
+-- Einstellungen
+------------------------------------------------------------------------
+
+if UI.RegisterSettingsSection then
+ UI:RegisterSettingsSection("mail", {
+ order = 118,
+ height = 190,
+ build = function(parent)
+ local f, stack = UI.CreateSection(parent, T("MAIL_SET_HEADER"))
+ local boxes = {}
+ local replace = UI.CreateCheckbox(f, T("MAIL_SET_REPLACE"), function()
+ return Mail.db.settings.replace
+ end, function(value)
+ Mail:SetReplace(value)
+ end)
+ stack:Add(replace, { fit = true, gap = 4 })
+ boxes[#boxes + 1] = replace
+ local chat = UI.CreateCheckbox(f, T("MAIL_SET_CHAT"), function()
+ return Mail.db.settings.chat
+ end, function(value)
+ Mail.db.settings.chat = value and true or false
+ end)
+ stack:Add(chat, { fit = true, gap = 4 })
+ boxes[#boxes + 1] = chat
+ local note = UI.CreateLabel(f, T("MAIL_SET_NOTE"), { size = 11, color = "textDim", justifyV = "TOP" })
+ stack:Add(note, { fit = true, gap = 10 })
+ function f.Refresh()
+ for _, cb in ipairs(boxes) do cb:Refresh() end
+ end
+ stack:Layout()
+ return f
+ end,
+ refresh = function(frame)
+ frame.Refresh()
+ end,
+ })
+end
+
+------------------------------------------------------------------------
+-- Befehl
+------------------------------------------------------------------------
+
+SLASH_FREEWOODMAIL1 = "/fwmail"
+SlashCmdList["FREEWOODMAIL"] = function(msg)
+ msg = (msg or ""):lower():match("^%s*(.-)%s*$")
+ if msg == "on" then
+ Mail:SetReplace(true)
+ FW:Print(T("MAIL_CMD_ON"))
+ elseif msg == "off" then
+ Mail:SetReplace(false)
+ FW:Print(T("MAIL_CMD_OFF"))
+ elseif Mail.open then
+ if window and window:IsShown() then HideWindow() else ShowWindow() end
+ else
+ FW:Print(T("MAIL_CMD_CLOSED"))
+ end
+end
diff --git a/README.md b/README.md
index 802073b..f7cbf33 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1 @@
# Freewood-OctoWoW-Mail
-Freewood-OctoWoW-Mail