pax_global_header00006660000000000000000000000064121700222320014501gustar00rootroot0000000000000052 comment=2df8f92f873f8dab4068d927f062c02ad7c863e0 Radicale-0.8/000077500000000000000000000000001217002223200130545ustar00rootroot00000000000000Radicale-0.8/.gitignore000066400000000000000000000001151217002223200150410ustar00rootroot00000000000000*~ *.pyc build dist .coverage .project .pydevproject .settings .tox MANIFEST Radicale-0.8/.pylintrc000066400000000000000000000152121217002223200147220ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. #ignore=CVS # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time. # Remove warning removal warning # Remove stupid warning on ** magic # Remove stupid reimport warning disable=I0011,W0142,W0404 [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html #output-format=colorized # Include message's id in output #include-ids=yes # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". #files-output=no # Tells whether to display a full report or only the messages #reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (R0004). comment=no [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=ParseResult,radicale.config,ldap # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. generated-members= [FORMAT] # Maximum number of characters on a single line. max-line-length=79 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=yes # A regular expression matching names used for dummy variables (i.e. not used). dummy-variables-rgx=_ # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [BASIC] # Required attributes for module, separated by a comma required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods= # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__ [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branchs=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= Radicale-0.8/COPYING000066400000000000000000001045131217002223200141130ustar00rootroot00000000000000 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 . Radicale-0.8/MANIFEST.in000066400000000000000000000001561217002223200146140ustar00rootroot00000000000000include COPYING NEWS.rst TODO.rst README.rst config logging radicale.py radicale.fcgi radicale.wsgi setup.sql Radicale-0.8/NEWS.rst000066400000000000000000000052161217002223200143660ustar00rootroot00000000000000====== News ====== 0.8 - Rainbow ============= * New authentication and rights management modules (by Matthias Jordan) * Experimental database storage * Command-line option for custom configuration file (by Mark Adams) * Root URL not at the root of a domain (by Clint Adams, Fabrice Bellet, Vincent Untz) * Improved support for iCal, CalDAVSync, CardDAVSync, CalDavZAP and CardDavMATE * Empty PROPFIND requests handled (by Christoph Polcin) * Colon allowed in passwords * Configurable realm message 0.7.1 - Waterfalls ================== * Many address books fixes * New IMAP ACL (by Daniel Aleksandersen) * PAM ACL fixed (by Daniel Aleksandersen) * Courier ACL fixed (by Benjamin Frank) * Always set display name to collections (by Oskari Timperi) * Various DELETE responses fixed 0.7 - Eternal Sunshine ====================== * Repeating events * Collection deletion * Courier and PAM authentication methods * CardDAV support * Custom LDAP filters supported 0.6.4 - Tulips ============== * Fix the installation with Python 3.1 0.6.3 - Red Roses ================= * MOVE requests fixed * Faster REPORT answers * Executable script moved into the package 0.6.2 - Seeds ============= * iPhone and iPad support fixed * Backslashes replaced by slashes in PROPFIND answers on Windows * PyPI archive set as default download URL 0.6.1 - Growing Up ================== * Example files included in the tarball * htpasswd support fixed * Redirection loop bug fixed * Testing message on GET requests 0.6 - Sapling ============= * WSGI support * IPv6 support * Smart, verbose and configurable logs * Apple iCal 4 and iPhone support (by Łukasz Langa) * KDE KOrganizer support * LDAP auth backend (by Corentin Le Bail) * Public and private calendars (by René Neumann) * PID file * MOVE requests management * Journal entries support * Drop Python 2.5 support 0.5 - Historical Artifacts ========================== * Calendar depth * MacOS and Windows support * HEAD requests management * htpasswd user from calendar path 0.4 - Hot Days Back =================== * Personal calendars * Last-Modified HTTP header * ``no-ssl`` and ``foreground`` options * Default configuration file 0.3 - Dancing Flowers ===================== * Evolution support * Version management 0.2 - Snowflakes ================ * Sunbird pre-1.0 support * SSL connection * Htpasswd authentication * Daemon mode * User configuration * Twisted dependency removed * Python 3 support * Real URLs for PUT and DELETE * Concurrent modification reported to users * Many bugs fixed (by Roger Wenham) 0.1 - Crazy Vegetables ====================== * First release * Lightning/Sunbird 0.9 compatibility * Easy installer Radicale-0.8/README000066400000000000000000000002741217002223200137370ustar00rootroot00000000000000The Radicale Project is a free and open-source CalDAV calendar server. For complete documentation, please visit the Radicale online documentation (http://www.radicale.org/documentation). Radicale-0.8/README.rst000066400000000000000000000003341217002223200145430ustar00rootroot00000000000000========= Read Me ========= The Radicale Project is a free and open-source CalDAV calendar server. For complete documentation, please visit the `Radicale online documentation `_ Radicale-0.8/TODO.rst000066400000000000000000000003021217002223200143460ustar00rootroot00000000000000============ To-Do List ============ 1.0 === * [IN PROGRESS] Other CalDAV clients supports * Git storage backend * Tests 2.0 === * iCal filters and rights * CalDAV rights * CalDAV filters Radicale-0.8/bin/000077500000000000000000000000001217002223200136245ustar00rootroot00000000000000Radicale-0.8/bin/radicale000077500000000000000000000017551217002223200153260ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale CalDAV Server. Launch the server according to configuration and command-line options. """ import radicale.__main__ radicale.__main__.run() Radicale-0.8/config000066400000000000000000000070711217002223200142510ustar00rootroot00000000000000# -*- mode: conf -*- # vim:ft=cfg # Config file for Radicale - A simple calendar server # # Place it into /etc/radicale/config (global) # or ~/.config/radicale/config (user) # # The current values are the default ones [server] # CalDAV server hostnames separated by a comma # IPv4 syntax: address:port # IPv6 syntax: [address]:port # IPv6 adresses are configured to only allow IPv6 connections hosts = 0.0.0.0:5232 # Daemon flag daemon = False # File storing the PID in daemon mode pid = # SSL flag, enable HTTPS protocol ssl = False # SSL certificate path certificate = /etc/apache2/ssl/server.crt # SSL private key key = /etc/apache2/ssl/server.key # Reverse DNS to resolve client address in logs dns_lookup = True # Root URL of Radicale (starting and ending with a slash) base_prefix = / # Message displayed in the client when a password is needed realm = Radicale - Password Required lol [encoding] # Encoding for responding requests request = utf-8 # Encoding for storing local collections stock = utf-8 [auth] # Authentication method # Value: None | htpasswd | IMAP | LDAP | PAM | courier | http type = None # Usernames used for public collections, separated by a comma public_users = public # Usernames used for private collections, separated by a comma private_users = private # Htpasswd filename htpasswd_filename = /etc/radicale/users # Htpasswd encryption method # Value: plain | sha1 | crypt htpasswd_encryption = crypt # LDAP server URL, with protocol and port ldap_url = ldap://localhost:389/ # LDAP base path ldap_base = ou=users,dc=example,dc=com # LDAP login attribute ldap_attribute = uid # LDAP filter string # placed as X in a query of the form (&(...)X) # example: (objectCategory=Person)(objectClass=User)(memberOf=cn=calenderusers,ou=users,dc=example,dc=org) # leave empty if no additional filter is needed ldap_filter = # LDAP dn for initial login, used if LDAP server does not allow anonymous searches # Leave empty if searches are anonymous ldap_binddn = # LDAP password for initial login, used with ldap_binddn ldap_password = # LDAP scope of the search ldap_scope = OneLevel # IMAP Configuration imap_hostname = localhost imap_port = 143 imap_ssl = False # PAM group user should be member of pam_group_membership = # Path to the Courier Authdaemon socket courier_socket = # HTTP authentication request URL endpoint http_url = # POST parameter to use for username http_user_parameter = # POST parameter to use for password http_password_parameter = [rights] # Rights management method # Value: None | owner_only | owner_write | from_file type = None # File for rights management from_file file = ~/.config/radicale/rights [storage] # Storage backend # Value: filesystem | database type = filesystem # Folder for storing local collections, created if not present filesystem_folder = ~/.config/radicale/collections # Database URL for SQLAlchemy # dialect+driver://user:password@host/dbname[?key=value..] # For example: sqlite:///var/db/radicale.db, postgresql://user:password@localhost/radicale # See http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html#sqlalchemy.create_engine database_url = [logging] # Logging configuration file # If no config is given, simple information is printed on the standard output # For more information about the syntax of the configuration file, see: # http://docs.python.org/library/logging.config.html config = /etc/radicale/logging # Set the default logging level to debug debug = False # Store all environment variables (including those set in the shell) full_environment = False # Additional HTTP headers #[headers] #Access-Control-Allow-Origin = * Radicale-0.8/logging000066400000000000000000000020121217002223200144200ustar00rootroot00000000000000# -*- mode: conf -*- # vim:ft=cfg # Logging config file for Radicale - A simple calendar server # # The default path for this file is /etc/radicale/logging # This can be changed in the configuration file # # Other handlers are available. For more information, see: # http://docs.python.org/library/logging.config.html # Loggers, handlers and formatters keys [loggers] # Loggers names, main configuration slots keys = root [handlers] # Logging handlers, defining logging output methods keys = console,file [formatters] # Logging formatters keys = simple,full # Loggers [logger_root] # Root logger level = DEBUG handlers = console,file # Handlers [handler_console] # Console handler class = StreamHandler level = INFO args = (sys.stdout,) formatter = simple [handler_file] # File handler class = FileHandler args = ('/var/log/radicale',) formatter = full # Formatters [formatter_simple] # Simple output format format = %(message)s [formatter_full] # Full output format format = %(asctime)s - %(levelname)s: %(message)s Radicale-0.8/radicale.fcgi000077500000000000000000000021371217002223200154600ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale FastCGI Example. Launch a Radicale FastCGI server according to configuration. """ from flup.server.fcgi import WSGIServer import radicale radicale.log.start() radicale.log.LOGGER.info("Starting Radicale FastCGI server") WSGIServer(radicale.Application()).run() radicale.log.LOGGER.info("Stopping Radicale FastCGI server") Radicale-0.8/radicale.py000077500000000000000000000017551217002223200152050ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale CalDAV Server. Launch the server according to configuration and command-line options. """ import radicale.__main__ radicale.__main__.run() Radicale-0.8/radicale.wsgi000077500000000000000000000016241217002223200155210ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale WSGI file (mod_wsgi and uWSGI compliant). """ import radicale radicale.log.start() application = radicale.Application() Radicale-0.8/radicale/000077500000000000000000000000001217002223200146205ustar00rootroot00000000000000Radicale-0.8/radicale/__init__.py000066400000000000000000000531041217002223200167340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale Server module. This module offers a WSGI application class. To use this module, you should take a look at the file ``radicale.py`` that should have been included in this package. """ import os import pprint import base64 import posixpath import socket import ssl import wsgiref.simple_server # Manage Python2/3 different modules # pylint: disable=F0401,E0611 try: from http import client from urllib.parse import unquote, urlparse except ImportError: import httplib as client from urllib import unquote from urlparse import urlparse # pylint: enable=F0401,E0611 from . import auth, config, ical, log, rights, storage, xmlutils VERSION = "0.8" # Standard "not allowed" response that is returned when an authenticated user # tries to access information they don't have rights to NOT_ALLOWED = (client.FORBIDDEN, {}, None) # Standard "authenticate" response that is returned when a user tries to access # non-public information w/o submitting proper authentication credentials WRONG_CREDENTIALS = ( client.UNAUTHORIZED, {"WWW-Authenticate": "Basic realm=\"%s\"" % config.get("server", "realm")}, None) class HTTPServer(wsgiref.simple_server.WSGIServer, object): """HTTP server.""" def __init__(self, address, handler, bind_and_activate=True): """Create server.""" ipv6 = ":" in address[0] if ipv6: self.address_family = socket.AF_INET6 # Do not bind and activate, as we might change socket options super(HTTPServer, self).__init__(address, handler, False) if ipv6: # Only allow IPv6 connections to the IPv6 socket self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) if bind_and_activate: self.server_bind() self.server_activate() class HTTPSServer(HTTPServer): """HTTPS server.""" def __init__(self, address, handler): """Create server by wrapping HTTP socket in an SSL socket.""" super(HTTPSServer, self).__init__(address, handler, False) # Test if the SSL files can be read for name in ("certificate", "key"): filename = config.get("server", name) try: open(filename, "r").close() except IOError as exception: log.LOGGER.warn( "Error while reading SSL %s %r: %s" % ( name, filename, exception)) self.socket = ssl.wrap_socket( self.socket, server_side=True, certfile=config.get("server", "certificate"), keyfile=config.get("server", "key"), ssl_version=ssl.PROTOCOL_SSLv23) self.server_bind() self.server_activate() class RequestHandler(wsgiref.simple_server.WSGIRequestHandler): """HTTP requests handler.""" def log_message(self, *args, **kwargs): """Disable inner logging management.""" def address_string(self): """Client address, formatted for logging.""" if config.getboolean("server", "dns_lookup"): return \ wsgiref.simple_server.WSGIRequestHandler.address_string(self) else: return self.client_address[0] class Application(object): """WSGI application managing collections.""" def __init__(self): """Initialize application.""" super(Application, self).__init__() auth.load() rights.load() storage.load() self.encoding = config.get("encoding", "request") if config.getboolean("logging", "full_environment"): self.headers_log = lambda environ: environ # This method is overriden in __init__ if full_environment is set # pylint: disable=E0202 @staticmethod def headers_log(environ): """Remove environment variables from the headers for logging.""" request_environ = dict(environ) for shell_variable in os.environ: if shell_variable in request_environ: del request_environ[shell_variable] return request_environ # pylint: enable=E0202 def decode(self, text, environ): """Try to magically decode ``text`` according to given ``environ``.""" # List of charsets to try charsets = [] # First append content charset given in the request content_type = environ.get("CONTENT_TYPE") if content_type and "charset=" in content_type: charsets.append(content_type.split("charset=")[1].strip()) # Then append default Radicale charset charsets.append(self.encoding) # Then append various fallbacks charsets.append("utf-8") charsets.append("iso8859-1") # Try to decode for charset in charsets: try: return text.decode(charset) except UnicodeDecodeError: pass raise UnicodeDecodeError @staticmethod def sanitize_uri(uri): """Unquote and remove /../ to prevent access to other data.""" uri = unquote(uri) trailing_slash = "/" if uri.endswith("/") else "" uri = posixpath.normpath(uri) trailing_slash = "" if uri == "/" else trailing_slash return uri + trailing_slash def collect_allowed_items(self, items, user): """Get items from request that user is allowed to access.""" read_last_collection_allowed = None write_last_collection_allowed = None read_allowed_items = [] write_allowed_items = [] for item in items: if isinstance(item, ical.Collection): if rights.read_authorized(user, item): log.LOGGER.debug( "%s has read access to collection %s" % (user or "Anonymous", item.url or "/")) read_last_collection_allowed = True read_allowed_items.append(item) else: log.LOGGER.debug( "%s has NO read access to collection %s" % (user or "Anonymous", item.url or "/")) read_last_collection_allowed = False if rights.write_authorized(user, item): log.LOGGER.debug( "%s has write access to collection %s" % (user or "Anonymous", item.url or "/")) write_last_collection_allowed = True write_allowed_items.append(item) else: log.LOGGER.debug( "%s has NO write access to collection %s" % (user or "Anonymous", item.url or "/")) write_last_collection_allowed = False else: # item is not a collection, it's the child of the last # collection we've met in the loop. Only add this item # if this last collection was allowed. if read_last_collection_allowed: log.LOGGER.debug( "%s has read access to item %s" % (user or "Anonymous", item.name)) read_allowed_items.append(item) else: log.LOGGER.debug( "%s has NO read access to item %s" % (user or "Anonymous", item.name)) if write_last_collection_allowed: log.LOGGER.debug( "%s has write access to item %s" % (user or "Anonymous", item.name)) write_allowed_items.append(item) else: log.LOGGER.debug( "%s has NO write access to item %s" % (user or "Anonymous", item.name)) return read_allowed_items, write_allowed_items def __call__(self, environ, start_response): """Manage a request.""" log.LOGGER.info("%s request at %s received" % ( environ["REQUEST_METHOD"], environ["PATH_INFO"])) headers = pprint.pformat(self.headers_log(environ)) log.LOGGER.debug("Request headers:\n%s" % headers) base_prefix = config.get("server", "base_prefix") if environ["PATH_INFO"].startswith(base_prefix): # Sanitize request URI environ["PATH_INFO"] = self.sanitize_uri( "/%s" % environ["PATH_INFO"][len(base_prefix):]) log.LOGGER.debug("Sanitized path: %s", environ["PATH_INFO"]) else: # Request path not starting with base_prefix, not allowed log.LOGGER.debug( "Path not starting with prefix: %s", environ["PATH_INFO"]) environ["PATH_INFO"] = None # Get content content_length = int(environ.get("CONTENT_LENGTH") or 0) if content_length: content = self.decode( environ["wsgi.input"].read(content_length), environ) log.LOGGER.debug("Request content:\n%s" % content) else: content = None path = environ["PATH_INFO"] # Find collection(s) items = ical.Collection.from_path(path, environ.get("HTTP_DEPTH", "0")) # Get function corresponding to method function = getattr(self, environ["REQUEST_METHOD"].lower()) # Ask authentication backend to check rights authorization = environ.get("HTTP_AUTHORIZATION", None) if authorization: authorization = \ authorization.lstrip("Basic").strip().encode("ascii") user, password = self.decode( base64.b64decode(authorization), environ).split(":", 1) else: user = password = None if not items or function == self.options or \ auth.is_authenticated(user, password): read_allowed_items, write_allowed_items = \ self.collect_allowed_items(items, user) if read_allowed_items or write_allowed_items or \ function == self.options or not items: # Collections found, or OPTIONS request, or no items at all status, headers, answer = function( environ, read_allowed_items, write_allowed_items, content, user) else: # Good user but has no rights to any of the given collections status, headers, answer = NOT_ALLOWED else: # Unknown or unauthorized user log.LOGGER.info( "%s refused" % (user or "Anonymous user")) status, headers, answer = WRONG_CREDENTIALS # Set content length if answer: log.LOGGER.debug( "Response content:\n%s" % self.decode(answer, environ)) headers["Content-Length"] = str(len(answer)) if config.has_section("headers"): for key in config.options("headers"): headers[key] = config.get("headers", key) # Start response status = "%i %s" % (status, client.responses.get(status, "Unknown")) log.LOGGER.debug("Answer status: %s" % status) start_response(status, list(headers.items())) # Return response content return [answer] if answer else [] # All these functions must have the same parameters, some are useless # pylint: disable=W0612,W0613,R0201 def delete(self, environ, read_collections, write_collections, content, user): """Manage DELETE request.""" if not len(write_collections): return client.PRECONDITION_FAILED, {}, None collection = write_collections[0] if collection.path == environ["PATH_INFO"].strip("/"): # Path matching the collection, the collection must be deleted item = collection else: # Try to get an item matching the path item = collection.get_item( xmlutils.name_from_path(environ["PATH_INFO"], collection)) if item: # Evolution bug workaround etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "") if etag == item.etag: # No ETag precondition or precondition verified, delete item answer = xmlutils.delete(environ["PATH_INFO"], collection) return client.OK, {}, answer # No item or ETag precondition not verified, do not delete item return client.PRECONDITION_FAILED, {}, None def get(self, environ, read_collections, write_collections, content, user): """Manage GET request. In Radicale, GET requests create collections when the URL is not available. This is useful for clients with no MKCOL or MKCALENDAR support. """ # Display a "Radicale works!" message if the root URL is requested if environ["PATH_INFO"] == "/": headers = {"Content-type": "text/html"} answer = b"\nRadicaleRadicale works!" return client.OK, headers, answer if not len(read_collections): return NOT_ALLOWED collection = read_collections[0] item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection) if item_name: # Get collection item item = collection.get_item(item_name) if item: items = collection.timezones items.append(item) answer_text = ical.serialize( collection.tag, collection.headers, items) etag = item.etag else: return client.GONE, {}, None else: # Create the collection if it does not exist if not collection.exists: if collection in write_collections: log.LOGGER.debug( "Creating collection %s" % collection.name) collection.write() else: log.LOGGER.debug( "Collection %s not available and could not be created " "due to missing write rights" % collection.name) return NOT_ALLOWED # Get whole collection answer_text = collection.text etag = collection.etag headers = { "Content-Type": collection.mimetype, "Last-Modified": collection.last_modified, "ETag": etag} answer = answer_text.encode(self.encoding) return client.OK, headers, answer def head(self, environ, read_collections, write_collections, content, user): """Manage HEAD request.""" status, headers, answer = self.get( environ, read_collections, write_collections, content, user) return status, headers, None def mkcalendar(self, environ, read_collections, write_collections, content, user): """Manage MKCALENDAR request.""" if not len(write_collections): return NOT_ALLOWED collection = write_collections[0] props = xmlutils.props_from_request(content) timezone = props.get("C:calendar-timezone") if timezone: collection.replace("", timezone) del props["C:calendar-timezone"] with collection.props as collection_props: for key, value in props.items(): collection_props[key] = value collection.write() return client.CREATED, {}, None def mkcol(self, environ, read_collections, write_collections, content, user): """Manage MKCOL request.""" if not len(write_collections): return NOT_ALLOWED collection = write_collections[0] props = xmlutils.props_from_request(content) with collection.props as collection_props: for key, value in props.items(): collection_props[key] = value collection.write() return client.CREATED, {}, None def move(self, environ, read_collections, write_collections, content, user): """Manage MOVE request.""" if not len(write_collections): return NOT_ALLOWED from_collection = write_collections[0] from_name = xmlutils.name_from_path( environ["PATH_INFO"], from_collection) if from_name: item = from_collection.get_item(from_name) if item: # Move the item to_url_parts = urlparse(environ["HTTP_DESTINATION"]) if to_url_parts.netloc == environ["HTTP_HOST"]: to_url = to_url_parts.path to_path, to_name = to_url.rstrip("/").rsplit("/", 1) to_collection = ical.Collection.from_path( to_path, depth="0")[0] if to_collection in write_collections: to_collection.append(to_name, item.text) from_collection.remove(from_name) return client.CREATED, {}, None else: return NOT_ALLOWED else: # Remote destination server, not supported return client.BAD_GATEWAY, {}, None else: # No item found return client.GONE, {}, None else: # Moving collections, not supported return client.FORBIDDEN, {}, None def options(self, environ, read_collections, write_collections, content, user): """Manage OPTIONS request.""" headers = { "Allow": ("DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " "OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT"), "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"} return client.OK, headers, None def propfind(self, environ, read_collections, write_collections, content, user): """Manage PROPFIND request.""" # Rights is handled by collection in xmlutils.propfind headers = { "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol", "Content-Type": "text/xml"} collections = set(read_collections + write_collections) answer = xmlutils.propfind( environ["PATH_INFO"], content, collections, user) return client.MULTI_STATUS, headers, answer def proppatch(self, environ, read_collections, write_collections, content, user): """Manage PROPPATCH request.""" if not len(write_collections): return NOT_ALLOWED collection = write_collections[0] answer = xmlutils.proppatch( environ["PATH_INFO"], content, collection) headers = { "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol", "Content-Type": "text/xml"} return client.MULTI_STATUS, headers, answer def put(self, environ, read_collections, write_collections, content, user): """Manage PUT request.""" if not len(write_collections): return NOT_ALLOWED collection = write_collections[0] collection.set_mimetype(environ.get("CONTENT_TYPE")) headers = {} item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection) item = collection.get_item(item_name) # Evolution bug workaround etag = environ.get("HTTP_IF_MATCH", "").replace("\\", "") match = environ.get("HTTP_IF_NONE_MATCH", "") == "*" if (not item and not etag) or ( item and ((etag or item.etag) == item.etag) and not match): # PUT allowed in 3 cases # Case 1: No item and no ETag precondition: Add new item # Case 2: Item and ETag precondition verified: Modify item # Case 3: Item and no Etag precondition: Force modifying item xmlutils.put(environ["PATH_INFO"], content, collection) status = client.CREATED # Try to return the etag in the header. # If the added item does't have the same name as the one given # by the client, then there's no obvious way to generate an # etag, we can safely ignore it. new_item = collection.get_item(item_name) if new_item: headers["ETag"] = new_item.etag else: # PUT rejected in all other cases status = client.PRECONDITION_FAILED return status, headers, None def report(self, environ, read_collections, write_collections, content, user): """Manage REPORT request.""" if not len(read_collections): return NOT_ALLOWED collection = read_collections[0] headers = {"Content-Type": "text/xml"} answer = xmlutils.report(environ["PATH_INFO"], content, collection) return client.MULTI_STATUS, headers, answer # pylint: enable=W0612,W0613,R0201 Radicale-0.8/radicale/__main__.py000066400000000000000000000147621217002223200167240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale executable module. This module can be executed from a command line with ``$python -m radicale`` or from a python programme with ``radicale.__main__.run()``. """ import atexit import os import sys import optparse import signal import threading from wsgiref.simple_server import make_server from . import ( Application, config, HTTPServer, HTTPSServer, log, RequestHandler, VERSION) # This is a script, many branches and variables # pylint: disable=R0912,R0914 def run(): """Run Radicale as a standalone server.""" # Get command-line options parser = optparse.OptionParser(version=VERSION) parser.add_option( "-d", "--daemon", action="store_true", help="launch as daemon") parser.add_option( "-p", "--pid", help="set PID filename for daemon mode") parser.add_option( "-f", "--foreground", action="store_false", dest="daemon", help="launch in foreground (opposite of --daemon)") parser.add_option( "-H", "--hosts", help="set server hostnames and ports") parser.add_option( "-s", "--ssl", action="store_true", help="use SSL connection") parser.add_option( "-S", "--no-ssl", action="store_false", dest="ssl", help="do not use SSL connection (opposite of --ssl)") parser.add_option( "-k", "--key", help="set private key file") parser.add_option( "-c", "--certificate", help="set certificate file") parser.add_option( "-D", "--debug", action="store_true", help="print debug information") parser.add_option( "-C", "--config", help="use a specific configuration file") options = parser.parse_args()[0] # Read in the configuration specified by the command line (if specified) configuration_found = ( config.read(options.config) if options.config else True) # Update Radicale configuration according to options for option in parser.option_list: key = option.dest if key: section = "logging" if key == "debug" else "server" value = getattr(options, key) if value is not None: config.set(section, key, str(value)) # Start logging log.start() # Log a warning if the configuration file of the command line is not found if not configuration_found: log.LOGGER.warning( "Configuration file '%s' not found" % options.config) # Fork if Radicale is launched as daemon if config.getboolean("server", "daemon"): if os.path.exists(config.get("server", "pid")): raise OSError("PID file exists: %s" % config.get("server", "pid")) pid = os.fork() if pid: try: if config.get("server", "pid"): open(config.get("server", "pid"), "w").write(str(pid)) finally: sys.exit() sys.stdout = sys.stderr = open(os.devnull, "w") # Register exit function def cleanup(): """Remove the PID files.""" log.LOGGER.debug("Cleaning up") # Remove PID file if (config.get("server", "pid") and config.getboolean("server", "daemon")): os.unlink(config.get("server", "pid")) atexit.register(cleanup) log.LOGGER.info("Starting Radicale") # Create collection servers servers = [] server_class = ( HTTPSServer if config.getboolean("server", "ssl") else HTTPServer) shutdown_program = threading.Event() for host in config.get("server", "hosts").split(","): address, port = host.strip().rsplit(":", 1) address, port = address.strip("[] "), int(port) servers.append( make_server(address, port, Application(), server_class, RequestHandler)) # SIGTERM and SIGINT (aka KeyboardInterrupt) should just mark this for # shutdown signal.signal(signal.SIGTERM, lambda *_: shutdown_program.set()) signal.signal(signal.SIGINT, lambda *_: shutdown_program.set()) def serve_forever(server): """Serve a server forever, cleanly shutdown when things go wrong.""" try: server.serve_forever() finally: shutdown_program.set() log.LOGGER.debug( "Base URL prefix: %s" % config.get("server", "base_prefix")) # Start the servers in a different loop to avoid possible race-conditions, # when a server exists but another server is added to the list at the same # time for server in servers: log.LOGGER.debug( "Listening to %s port %s" % ( server.server_name, server.server_port)) if config.getboolean("server", "ssl"): log.LOGGER.debug("Using SSL") threading.Thread(target=serve_forever, args=(server,)).start() log.LOGGER.debug("Radicale server ready") # Main loop: wait until all servers are exited try: # We must do the busy-waiting here, as all ``.join()`` calls completly # block the thread, such that signals are not received while True: # The number is irrelevant, it only needs to be greater than 0.05 # due to python implementing its own busy-waiting logic shutdown_program.wait(5.0) if shutdown_program.is_set(): break finally: # Ignore signals, so that they cannot interfere signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGTERM, signal.SIG_IGN) log.LOGGER.info("Stopping Radicale") for server in servers: log.LOGGER.debug( "Closing server listening to %s port %s" % ( server.server_name, server.server_port)) server.shutdown() # pylint: enable=R0912,R0914 if __name__ == "__main__": run() Radicale-0.8/radicale/auth/000077500000000000000000000000001217002223200155615ustar00rootroot00000000000000Radicale-0.8/radicale/auth/IMAP.py000066400000000000000000000067051217002223200166710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012 Daniel Aleksandersen # Copyright © 2013 Nikita Koshikov # Copyright © 2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ IMAP authentication. Secure authentication based on the ``imaplib`` module. Validating users against a modern IMAP4rev1 server that awaits STARTTLS on port 143. Legacy SSL (often on legacy port 993) is deprecated and thus unsupported. STARTTLS is enforced except if host is ``localhost`` as passwords are sent in PLAIN. Python 3.2 or newer is required for TLS. """ import imaplib from .. import config, log IMAP_SERVER = config.get("auth", "imap_hostname") IMAP_SERVER_PORT = config.getint("auth", "imap_port") IMAP_USE_SSL = config.getboolean("auth", "imap_ssl") def is_authenticated(user, password): """Check if ``user``/``password`` couple is valid.""" if not user or not password: return False log.LOGGER.debug( "Connecting to IMAP server %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,)) connection_is_secure = False if IMAP_USE_SSL: connection = imaplib.IMAP4_SSL(host=IMAP_SERVER, port=IMAP_SERVER_PORT) connection_is_secure = True else: connection = imaplib.IMAP4(host=IMAP_SERVER, port=IMAP_SERVER_PORT) server_is_local = (IMAP_SERVER == "localhost") if not connection_is_secure: try: connection.starttls() log.LOGGER.debug("IMAP server connection changed to TLS.") connection_is_secure = True except AttributeError: if not server_is_local: log.LOGGER.error( "Python 3.2 or newer is required for IMAP + TLS.") except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception: log.LOGGER.warning( "IMAP server at %s failed to accept TLS connection " "because of: %s" % (IMAP_SERVER, exception)) if server_is_local and not connection_is_secure: log.LOGGER.warning( "IMAP server is local. " "Will allow transmitting unencrypted credentials.") if connection_is_secure or server_is_local: try: connection.login(user, password) connection.logout() log.LOGGER.debug( "Authenticated IMAP user %s " "via %s." % (user, IMAP_SERVER)) return True except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception: log.LOGGER.error( "IMAP server could not authenticate user %s " "because of: %s" % (user, exception)) else: log.LOGGER.critical( "IMAP server did not support TLS and is not ``localhost``. " "Refusing to transmit passwords under these conditions. " "Authentication attempt aborted.") return False # authentication failed Radicale-0.8/radicale/auth/LDAP.py000066400000000000000000000050461217002223200166600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011 Corentin Le Bail # Copyright © 2011-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ LDAP authentication. Authentication based on the ``python-ldap`` module (http://www.python-ldap.org/). """ import ldap from .. import config, log BASE = config.get("auth", "ldap_base") ATTRIBUTE = config.get("auth", "ldap_attribute") FILTER = config.get("auth", "ldap_filter") CONNEXION = ldap.initialize(config.get("auth", "ldap_url")) BINDDN = config.get("auth", "ldap_binddn") PASSWORD = config.get("auth", "ldap_password") SCOPE = getattr(ldap, "SCOPE_%s" % config.get("auth", "ldap_scope").upper()) def is_authenticated(user, password): """Check if ``user``/``password`` couple is valid.""" global CONNEXION try: CONNEXION.whoami_s() except: log.LOGGER.debug("Reconnecting the LDAP server") CONNEXION = ldap.initialize(config.get("auth", "ldap_url")) if BINDDN and PASSWORD: log.LOGGER.debug("Initial LDAP bind as %s" % BINDDN) CONNEXION.simple_bind_s(BINDDN, PASSWORD) distinguished_name = "%s=%s" % (ATTRIBUTE, ldap.dn.escape_dn_chars(user)) log.LOGGER.debug( "LDAP bind for %s in base %s" % (distinguished_name, BASE)) if FILTER: filter_string = "(&(%s)%s)" % (distinguished_name, FILTER) else: filter_string = distinguished_name log.LOGGER.debug("Used LDAP filter: %s" % filter_string) users = CONNEXION.search_s(BASE, SCOPE, filter_string) if users: log.LOGGER.debug("User %s found" % user) try: CONNEXION.simple_bind_s(users[0][0], password or "") except ldap.LDAPError: log.LOGGER.debug("Invalid credentials") else: log.LOGGER.debug("LDAP bind OK") return True else: log.LOGGER.debug("User %s not found" % user) log.LOGGER.debug("LDAP bind failed") return False Radicale-0.8/radicale/auth/PAM.py000066400000000000000000000042141217002223200165510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011 Henry-Nicolas Tourneur # # This library 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 library 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 Radicale. If not, see . """ PAM authentication. Authentication based on the ``pam-python`` module. """ import grp import pam import pwd from .. import config, log GROUP_MEMBERSHIP = config.get("auth", "pam_group_membership") def is_authenticated(user, password): """Check if ``user``/``password`` couple is valid.""" # Check whether the user exists in the PAM system try: pwd.getpwnam(user).pw_uid except KeyError: log.LOGGER.debug("User %s not found" % user) return False else: log.LOGGER.debug("User %s found" % user) # Check whether the group exists try: members = grp.getgrnam(GROUP_MEMBERSHIP).gr_mem except KeyError: log.LOGGER.debug( "The PAM membership required group (%s) doesn't exist" % GROUP_MEMBERSHIP) return False # Check whether the user belongs to the required group for member in members: if member == user: log.LOGGER.debug( "The PAM user belongs to the required group (%s)" % GROUP_MEMBERSHIP) # Check the password if pam.authenticate(user, password): return True else: log.LOGGER.debug("Wrong PAM password") break else: log.LOGGER.debug( "The PAM user doesn't belong to the required group (%s)" % GROUP_MEMBERSHIP) return False Radicale-0.8/radicale/auth/__init__.py000066400000000000000000000031371217002223200176760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Authentication management. """ import sys from .. import config, log def load(): """Load list of available authentication managers.""" auth_type = config.get("auth", "type") log.LOGGER.debug("Authentication type is %s" % auth_type) if auth_type == "None": return None else: root_module = __import__( "auth.%s" % auth_type, globals=globals(), level=2) module = getattr(root_module, auth_type) # Override auth.is_authenticated sys.modules[__name__].is_authenticated = module.is_authenticated return module def is_authenticated(user, password): """Check if the user is authenticated. This method is overriden if an auth module is loaded. """ return True # Default is always True: no authentication Radicale-0.8/radicale/auth/courier.py000066400000000000000000000040551217002223200176070ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011 Henry-Nicolas Tourneur # # This library 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 library 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 Radicale. If not, see . """ Courier-Authdaemon authentication. """ import sys import socket from .. import config, log COURIER_SOCKET = config.get("auth", "courier_socket") def is_authenticated(user, password): """Check if ``user``/``password`` couple is valid.""" if not user or not password: return False line = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password) line = "AUTH %i\n%s" % (len(line), line) try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(COURIER_SOCKET) log.LOGGER.debug("Sending to Courier socket the request: %s" % line) sock.send(line) data = sock.recv(1024) sock.close() except socket.error as exception: log.LOGGER.debug( "Unable to communicate with Courier socket: %s" % exception) return False log.LOGGER.debug("Got Courier socket response: %r" % data) # Address, HOME, GID, and either UID or USERNAME are mandatory in resposne # see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto for line in data.split(): if "GID" in line: return True # default is reject # this alleviates the problem of a possibly empty reply from authlib # see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto return False Radicale-0.8/radicale/auth/htpasswd.py000066400000000000000000000044751217002223200200020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Htpasswd authentication. Load the list of login/password couples according a the configuration file created by Apache ``htpasswd`` command. Plain-text, crypt and sha1 are supported, but md5 is not (see ``htpasswd`` man page to understand why). """ import base64 import hashlib from .. import config FILENAME = config.get("auth", "htpasswd_filename") ENCRYPTION = config.get("auth", "htpasswd_encryption") def _plain(hash_value, password): """Check if ``hash_value`` and ``password`` match using plain method.""" return hash_value == password def _crypt(hash_value, password): """Check if ``hash_value`` and ``password`` match using crypt method.""" # The ``crypt`` module is only present on Unix, import if needed import crypt return crypt.crypt(password, hash_value) == hash_value def _sha1(hash_value, password): """Check if ``hash_value`` and ``password`` match using sha1 method.""" hash_value = hash_value.replace("{SHA}", "").encode("ascii") password = password.encode(config.get("encoding", "stock")) sha1 = hashlib.sha1() # pylint: disable=E1101 sha1.update(password) return sha1.digest() == base64.b64decode(hash_value) def is_authenticated(user, password): """Check if ``user``/``password`` couple is valid.""" for line in open(FILENAME).readlines(): if line.strip(): login, hash_value = line.strip().split(":") if login == user: return globals()["_%s" % ENCRYPTION](hash_value, password) return False Radicale-0.8/radicale/auth/http.py000066400000000000000000000027351217002223200171210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012 Ehsanul Hoque # Copyright © 2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ HTTP authentication. Authentication based on the ``requests`` module. Post a request to an authentication server with the username/password. Anything other than a 200/201 response is considered auth failure. """ import requests from .. import config, log AUTH_URL = config.get("auth", "http_url") USER_PARAM = config.get("auth", "http_user_parameter") PASSWORD_PARAM = config.get("auth", "http_password_parameter") def is_authenticated(user, password): """Check if ``user``/``password`` couple is valid.""" log.LOGGER.debug("HTTP-based auth on %s." % AUTH_URL) payload = {USER_PARAM: user, PASSWORD_PARAM: password} return requests.post(AUTH_URL, data=payload).status_code in (200, 201) Radicale-0.8/radicale/config.py000066400000000000000000000062451217002223200164460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008-2013 Guillaume Ayoub # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # # This library 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 library 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 Radicale. If not, see . """ Radicale configuration module. Give a configparser-like interface to read and write configuration. """ import os import sys # Manage Python2/3 different modules # pylint: disable=F0401 try: from configparser import RawConfigParser as ConfigParser except ImportError: from ConfigParser import RawConfigParser as ConfigParser # pylint: enable=F0401 # Default configuration INITIAL_CONFIG = { "server": { "hosts": "0.0.0.0:5232", "daemon": "False", "pid": "", "ssl": "False", "certificate": "/etc/apache2/ssl/server.crt", "key": "/etc/apache2/ssl/server.key", "dns_lookup": "True", "base_prefix": "/", "realm": "Radicale - Password Required"}, "encoding": { "request": "utf-8", "stock": "utf-8"}, "auth": { "type": "None", "public_users": "public", "private_users": "private", "htpasswd_filename": "/etc/radicale/users", "htpasswd_encryption": "crypt", "imap_hostname": "localhost", "imap_port": "143", "imap_ssl": "False", "ldap_url": "ldap://localhost:389/", "ldap_base": "ou=users,dc=example,dc=com", "ldap_attribute": "uid", "ldap_filter": "", "ldap_binddn": "", "ldap_password": "", "ldap_scope": "OneLevel", "pam_group_membership": "", "courier_socket": "", "http_url": "", "http_user_parameter": "", "http_password_parameter": ""}, "rights": { "type": "None", "file": ""}, "storage": { "type": "filesystem", "filesystem_folder": os.path.expanduser( "~/.config/radicale/collections"), "database_url": ""}, "logging": { "config": "/etc/radicale/logging", "debug": "False", "full_environment": "False"}} # Create a ConfigParser and configure it _CONFIG_PARSER = ConfigParser() for section, values in INITIAL_CONFIG.items(): _CONFIG_PARSER.add_section(section) for key, value in values.items(): _CONFIG_PARSER.set(section, key, value) _CONFIG_PARSER.read("/etc/radicale/config") _CONFIG_PARSER.read(os.path.expanduser("~/.config/radicale/config")) if "RADICALE_CONFIG" in os.environ: _CONFIG_PARSER.read(os.environ["RADICALE_CONFIG"]) # Wrap config module into ConfigParser instance sys.modules[__name__] = _CONFIG_PARSER Radicale-0.8/radicale/ical.py000066400000000000000000000356411217002223200161130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale collection classes. Define the main classes of a collection as seen from the server. """ import os import posixpath import uuid from contextlib import contextmanager def serialize(tag, headers=(), items=()): """Return a text corresponding to given collection ``tag``. The text may have the given ``headers`` and ``items`` added around the items if needed (ie. for calendars). """ if tag == "VADDRESSBOOK": lines = [item.text for item in items] else: lines = ["BEGIN:%s" % tag] for part in (headers, items): if part: lines.append("\n".join(item.text for item in part)) lines.append("END:%s\n" % tag) return "\n".join(lines) def unfold(text): """Unfold multi-lines attributes. Read rfc5545-3.1 for info. """ lines = [] for line in text.splitlines(): if lines and (line.startswith(" ") or line.startswith("\t")): lines[-1] += line[1:] else: lines.append(line) return lines class Item(object): """Internal iCal item.""" def __init__(self, text, name=None): """Initialize object from ``text`` and different ``kwargs``.""" self.text = text self._name = name # We must synchronize the name in the text and in the object. # An item must have a name, determined in order by: # # - the ``name`` parameter # - the ``X-RADICALE-NAME`` iCal property (for Events, Todos, Journals) # - the ``UID`` iCal property (for Events, Todos, Journals) # - the ``TZID`` iCal property (for Timezones) if not self._name: for line in unfold(self.text): if line.startswith("X-RADICALE-NAME:"): self._name = line.replace("X-RADICALE-NAME:", "").strip() break elif line.startswith("TZID:"): self._name = line.replace("TZID:", "").strip() break elif line.startswith("UID:"): self._name = line.replace("UID:", "").strip() # Do not break, a ``X-RADICALE-NAME`` can appear next if self._name: # Remove brackets that may have been put by Outlook self._name = self._name.strip("{}") if "\nX-RADICALE-NAME:" in text: for line in unfold(self.text): if line.startswith("X-RADICALE-NAME:"): self.text = self.text.replace( line, "X-RADICALE-NAME:%s" % self._name) else: self.text = self.text.replace( "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name) else: self._name = str(uuid.uuid4()) self.text = self.text.replace( "\nEND:", "\nX-RADICALE-NAME:%s\nEND:" % self._name) @property def etag(self): """Item etag. Etag is mainly used to know if an item has changed. """ return '"%s"' % hash(self.text) @property def name(self): """Item name. Name is mainly used to give an URL to the item. """ return self._name class Header(Item): """Internal header class.""" class Timezone(Item): """Internal timezone class.""" tag = "VTIMEZONE" class Component(Item): """Internal main component of a collection.""" class Event(Component): """Internal event class.""" tag = "VEVENT" mimetype = "text/calendar" class Todo(Component): """Internal todo class.""" tag = "VTODO" # pylint: disable=W0511 mimetype = "text/calendar" class Journal(Component): """Internal journal class.""" tag = "VJOURNAL" mimetype = "text/calendar" class Card(Component): """Internal card class.""" tag = "VCARD" mimetype = "text/vcard" class Collection(object): """Internal collection item. This class must be overridden and replaced by a storage backend. """ def __init__(self, path, principal=False): """Initialize the collection. ``path`` must be the normalized relative path of the collection, using the slash as the folder delimiter, with no leading nor trailing slash. """ self.encoding = "utf-8" split_path = path.split("/") self.path = path if path != "." else "" if principal and split_path and self.is_node(self.path): # Already existing principal collection self.owner = split_path[0] elif len(split_path) > 1: # URL with at least one folder self.owner = split_path[0] else: self.owner = None self.is_principal = principal @classmethod def from_path(cls, path, depth="1", include_container=True): """Return a list of collections and items under the given ``path``. If ``depth`` is "0", only the actual object under ``path`` is returned. If ``depth`` is anything but "0", it is considered as "1" and direct children are included in the result. If ``include_container`` is ``True`` (the default), the containing object is included in the result. The ``path`` is relative. """ # path == None means wrong URL if path is None: return [] # First do normpath and then strip, to prevent access to FOLDER/../ sane_path = posixpath.normpath(path.replace(os.sep, "/")).strip("/") attributes = sane_path.split("/") if not attributes: return [] if not (cls.is_leaf("/".join(attributes)) or path.endswith("/")): attributes.pop() result = [] path = "/".join(attributes) principal = len(attributes) <= 1 if cls.is_node(path): if depth == "0": result.append(cls(path, principal)) else: if include_container: result.append(cls(path, principal)) for child in cls.children(path): result.append(child) else: if depth == "0": result.append(cls(path)) else: collection = cls(path, principal) if include_container: result.append(collection) result.extend(collection.components) return result def save(self, text): """Save the text into the collection.""" raise NotImplementedError def delete(self): """Delete the collection.""" raise NotImplementedError @property def text(self): """Collection as plain text.""" raise NotImplementedError @classmethod def children(cls, path): """Yield the children of the collection at local ``path``.""" raise NotImplementedError @classmethod def is_node(cls, path): """Return ``True`` if relative ``path`` is a node. A node is a WebDAV collection whose members are other collections. """ raise NotImplementedError @classmethod def is_leaf(cls, path): """Return ``True`` if relative ``path`` is a leaf. A leaf is a WebDAV collection whose members are not collections. """ raise NotImplementedError @property def last_modified(self): """Get the last time the collection has been modified. The date is formatted according to rfc1123-5.2.14. """ raise NotImplementedError @property @contextmanager def props(self): """Get the collection properties.""" raise NotImplementedError @property def exists(self): """``True`` if the collection exists on the storage, else ``False``.""" return self.is_node(self.path) or self.is_leaf(self.path) @staticmethod def _parse(text, item_types, name=None): """Find items with type in ``item_types`` in ``text``. If ``name`` is given, give this name to new items in ``text``. Return a list of items. """ item_tags = {} for item_type in item_types: item_tags[item_type.tag] = item_type items = {} lines = unfold(text) in_item = False for line in lines: if line.startswith("BEGIN:") and not in_item: item_tag = line.replace("BEGIN:", "").strip() if item_tag in item_tags: in_item = True item_lines = [] if in_item: item_lines.append(line) if line.startswith("END:%s" % item_tag): in_item = False item_type = item_tags[item_tag] item_text = "\n".join(item_lines) item_name = None if item_tag == "VTIMEZONE" else name item = item_type(item_text, item_name) if item.name in items: text = "\n".join((item.text, items[item.name].text)) items[item.name] = item_type(text, item.name) else: items[item.name] = item return list(items.values()) def get_item(self, name): """Get collection item called ``name``.""" for item in self.items: if item.name == name: return item def append(self, name, text): """Append items from ``text`` to collection. If ``name`` is given, give this name to new items in ``text``. """ items = self.items for new_item in self._parse( text, (Timezone, Event, Todo, Journal, Card), name): if new_item.name not in (item.name for item in items): items.append(new_item) self.write(items=items) def remove(self, name): """Remove object named ``name`` from collection.""" components = [ component for component in self.components if component.name != name] items = self.timezones + components self.write(items=items) def replace(self, name, text): """Replace content by ``text`` in collection objet called ``name``.""" self.remove(name) self.append(name, text) def write(self, headers=None, items=None): """Write collection with given parameters.""" headers = headers or self.headers or ( Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), Header("VERSION:%s" % self.version)) items = items if items is not None else self.items text = serialize(self.tag, headers, items) self.save(text) def set_mimetype(self, mimetype): """Set the mimetype of the collection.""" with self.props as props: if "tag" not in props: if mimetype == "text/vcard": props["tag"] = "VADDRESSBOOK" else: props["tag"] = "VCALENDAR" @property def tag(self): """Type of the collection.""" with self.props as props: if "tag" not in props: try: tag = open(self.path).readlines()[0][6:].rstrip() except IOError: if self.path.endswith(".vcf"): props["tag"] = "VADDRESSBOOK" else: props["tag"] = "VCALENDAR" else: if tag in ("VADDRESSBOOK", "VCARD"): props["tag"] = "VADDRESSBOOK" else: props["tag"] = "VCALENDAR" return props["tag"] @property def mimetype(self): """Mimetype of the collection.""" if self.tag == "VADDRESSBOOK": return "text/vcard" elif self.tag == "VCALENDAR": return "text/calendar" @property def resource_type(self): """Resource type of the collection.""" if self.tag == "VADDRESSBOOK": return "addressbook" elif self.tag == "VCALENDAR": return "calendar" @property def etag(self): """Etag from collection.""" return '"%s"' % hash(self.text) @property def name(self): """Collection name.""" with self.props as props: return props.get("D:displayname", self.path.split(os.path.sep)[-1]) @property def headers(self): """Find headers items in collection.""" header_lines = [] lines = unfold(self.text) for header in ("PRODID", "VERSION"): for line in lines: if line.startswith("%s:" % header): header_lines.append(Header(line)) break return header_lines @property def items(self): """Get list of all items in collection.""" return self._parse(self.text, (Event, Todo, Journal, Card, Timezone)) @property def components(self): """Get list of all components in collection.""" return self._parse(self.text, (Event, Todo, Journal, Card)) @property def events(self): """Get list of ``Event`` items in calendar.""" return self._parse(self.text, (Event,)) @property def todos(self): """Get list of ``Todo`` items in calendar.""" return self._parse(self.text, (Todo,)) @property def journals(self): """Get list of ``Journal`` items in calendar.""" return self._parse(self.text, (Journal,)) @property def timezones(self): """Get list of ``Timezome`` items in calendar.""" return self._parse(self.text, (Timezone,)) @property def cards(self): """Get list of ``Card`` items in address book.""" return self._parse(self.text, (Card,)) @property def owner_url(self): """Get the collection URL according to its owner.""" if self.owner: return "/%s/" % self.owner else: return None @property def url(self): """Get the standard collection URL.""" return "%s/" % self.path @property def version(self): """Get the version of the collection type.""" return "3.0" if self.tag == "VADDRESSBOOK" else "2.0" Radicale-0.8/radicale/log.py000066400000000000000000000035351217002223200157610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2011-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale logging module. Manage logging from a configuration file. For more information, see: http://docs.python.org/library/logging.config.html """ import os import sys import logging import logging.config from . import config LOGGER = logging.getLogger() def start(): """Start the logging according to the configuration.""" filename = os.path.expanduser(config.get("logging", "config")) debug = config.getboolean("logging", "debug") if os.path.exists(filename): # Configuration taken from file logging.config.fileConfig(filename) if debug: LOGGER.setLevel(logging.DEBUG) for handler in LOGGER.handlers: handler.setLevel(logging.DEBUG) else: # Default configuration, standard output handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter("%(message)s")) LOGGER.addHandler(handler) if debug: LOGGER.setLevel(logging.DEBUG) LOGGER.debug( "Logging configuration file '%s' not found, using stdout.", filename) Radicale-0.8/radicale/rights/000077500000000000000000000000001217002223200161205ustar00rootroot00000000000000Radicale-0.8/radicale/rights/__init__.py000066400000000000000000000035021217002223200202310ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Rights management. """ import sys from .. import config, log def load(): """Load list of available ACL managers.""" rights_type = config.get("rights", "type") log.LOGGER.debug("Rights type is %s" % rights_type) if rights_type == "None": return None else: root_module = __import__( "rights.%s" % rights_type, globals=globals(), level=2) module = getattr(root_module, rights_type) # Override rights.[read|write]_authorized sys.modules[__name__].read_authorized = module.read_authorized sys.modules[__name__].write_authorized = module.write_authorized return module def read_authorized(user, collection): """Check if the user is allowed to read the collection. This method is overriden if an auth module is loaded. """ return True def write_authorized(user, collection): """Check if the user is allowed to write the collection. This method is overriden if an auth module is loaded. """ return True Radicale-0.8/radicale/rights/from_file.py000066400000000000000000000057401217002223200204420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ File-based rights. The owners are implied to have all rights on their collections. Rights are read from a file whose name is specified in the config (section "right", key "file"). Example: # This means user1 may read, user2 may write, user3 has full access. [user0/calendar] user1: r user2: w user3: rw # user0 can read user1/cal. [user1/cal] user0: r # If a collection a/b is shared and other users than the owner are supposed to # find the collection in a propfind request, an additional line for a has to # be in the defintions. [user0] user1: r """ import os.path from radicale import config, log from radicale.rights import owner_only # Manage Python2/3 different modules # pylint: disable=F0401 try: from configparser import ( RawConfigParser as ConfigParser, NoSectionError, NoOptionError) except ImportError: from ConfigParser import ( RawConfigParser as ConfigParser, NoSectionError, NoOptionError) # pylint: enable=F0401 FILENAME = ( os.path.expanduser(config.get("rights", "file")) or log.LOGGER.error("No file name configured for rights type 'from_file'")) def _read_rights(): """Update the rights according to the configuration file.""" log.LOGGER.debug("Reading rights from file %s" % FILENAME) rights = ConfigParser() if not rights.read(FILENAME): log.LOGGER.error( "File '%s' not found for rights management" % FILENAME) return rights def read_authorized(user, collection): """Check if the user is allowed to read the collection.""" if user is None: return False elif owner_only.read_authorized(user, collection): return True else: try: return "r" in _read_rights().get( collection.url.rstrip("/") or "/", user) except (NoSectionError, NoOptionError): return False def write_authorized(user, collection): """Check if the user is allowed to write the collection.""" if user is None: return False elif owner_only.write_authorized(user, collection): return True else: try: return "w" in _read_rights().get( collection.url.rstrip("/") or "/", user) except (NoSectionError, NoOptionError): return False Radicale-0.8/radicale/rights/owner_only.py000066400000000000000000000021631217002223200206670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Owner-only based rights. Only owners have read and write access to their own collections. """ def read_authorized(user, collection): """Check if the user is allowed to read the collection.""" return user == collection.owner def write_authorized(user, collection): """Check if the user is allowed to write the collection.""" return user == collection.owner Radicale-0.8/radicale/rights/owner_write.py000066400000000000000000000022401217002223200210340ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Owner-only write based rights. Authenticated users have read access to all calendars, but only owners have write access to their own collections. """ def read_authorized(user, collection): """Check if the user is allowed to read the collection.""" return True def write_authorized(user, collection): """Check if the user is allowed to write the collection.""" return user and user == collection.owner Radicale-0.8/radicale/storage/000077500000000000000000000000001217002223200162645ustar00rootroot00000000000000Radicale-0.8/radicale/storage/__init__.py000066400000000000000000000022701217002223200203760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Storage backends. This module loads the storage backend, according to the storage configuration. """ from .. import config, ical def load(): """Load list of available storage managers.""" storage_type = config.get("storage", "type") root_module = __import__( "storage.%s" % storage_type, globals=globals(), level=2) module = getattr(root_module, storage_type) ical.Collection = module.Collection return module Radicale-0.8/radicale/storage/database.py000066400000000000000000000212511217002223200204030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ SQLAlchemy storage backend. """ import time from datetime import datetime from contextlib import contextmanager from sqlalchemy import create_engine, Column, String, DateTime, ForeignKey from sqlalchemy import func from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.ext.declarative import declarative_base from .. import config, ical # These are classes, not constants # pylint: disable=C0103 Base = declarative_base() Session = sessionmaker() Session.configure(bind=create_engine(config.get("storage", "database_url"))) # pylint: enable=C0103 class DBCollection(Base): """Table of collections.""" __tablename__ = "collection" path = Column(String, primary_key=True) parent_path = Column(String, ForeignKey("collection.path")) parent = relationship( "DBCollection", backref="children", remote_side=[path]) class DBItem(Base): """Table of collection's items.""" __tablename__ = "item" name = Column(String, primary_key=True) tag = Column(String) collection_path = Column(String, ForeignKey("collection.path")) collection = relationship("DBCollection", backref="items") class DBHeader(Base): """Table of item's headers.""" __tablename__ = "header" key = Column(String, primary_key=True) value = Column(String) collection_path = Column( String, ForeignKey("collection.path"), primary_key=True) collection = relationship("DBCollection", backref="headers") class DBLine(Base): """Table of item's lines.""" __tablename__ = "line" key = Column(String, primary_key=True) value = Column(String) item_name = Column(String, ForeignKey("item.name"), primary_key=True) timestamp = Column(DateTime, default=datetime.now) item = relationship( "DBItem", backref="lines", order_by=timestamp) class DBProperty(Base): """Table of collection's properties.""" __tablename__ = "property" key = Column(String, primary_key=True) value = Column(String) collection_path = Column( String, ForeignKey("collection.path"), primary_key=True) collection = relationship( "DBCollection", backref="properties", cascade="delete") class Collection(ical.Collection): """Collection stored in a database.""" def __init__(self, path, principal=False): self.session = Session() super(Collection, self).__init__(path, principal) def __del__(self): self.session.commit() def _query(self, item_types): """Get collection's items matching ``item_types``.""" item_objects = [] for item_type in item_types: items = ( self.session.query(DBItem) .filter_by(collection_path=self.path, tag=item_type.tag) .order_by("name").all()) for item in items: text = "\n".join( "%s:%s" % (line.key, line.value) for line in item.lines) item_objects.append(item_type(text, item.name)) return item_objects @property def _modification_time(self): """Collection's last modification time.""" return ( self.session.query(func.max(DBLine.timestamp)) .join(DBItem).filter_by(collection_path=self.path).first()[0] or datetime.now()) @property def _db_collection(self): """Collection's object mapped to the table line.""" return self.session.query(DBCollection).get(self.path) def write(self, headers=None, items=None): headers = headers or self.headers or ( ical.Header("PRODID:-//Radicale//NONSGML Radicale Server//EN"), ical.Header("VERSION:%s" % self.version)) items = items if items is not None else self.items if self._db_collection: for item in self._db_collection.items: for line in item.lines: self.session.delete(line) self.session.delete(item) for header in self._db_collection.headers: self.session.delete(header) else: db_collection = DBCollection() db_collection.path = self.path self.session.add(db_collection) for header in headers: db_header = DBHeader() db_header.key, db_header.value = header.text.split(":", 1) db_header.collection_path = self.path self.session.add(db_header) for item in items: db_item = DBItem() db_item.name = item.name db_item.tag = item.tag db_item.collection_path = self.path self.session.add(db_item) for line in ical.unfold(item.text): db_line = DBLine() db_line.key, db_line.value = line.split(":", 1) db_line.item_name = item.name self.session.add(db_line) def delete(self): self.session.delete(self._db_collection) @property def text(self): return ical.serialize(self.tag, self.headers, self.items) @property def etag(self): return '"%s"' % hash(self._modification_time) @property def headers(self): headers = ( self.session.query(DBHeader) .filter_by(collection_path=self.path) .order_by("key").all()) return [ ical.Header("%s:%s" % (header.key, header.value)) for header in headers] @classmethod def children(cls, path): session = Session() if path: children = session.query(DBCollection).get(path).children else: children = session.query(DBCollection).filter_by(parent=None).all() collections = [cls(child.path) for child in children] session.close() return collections @classmethod def is_node(cls, path): if not path: return True session = Session() result = ( session.query(DBCollection) .filter_by(parent_path=path).count() > 0) session.close() return result @classmethod def is_leaf(cls, path): if not path: return False session = Session() result = ( session.query(DBItem) .filter_by(collection_path=path).count() > 0) session.close() return result @property def last_modified(self): return time.strftime( "%a, %d %b %Y %H:%M:%S +0000", self._modification_time.timetuple()) @property @contextmanager def props(self): # On enter properties = {} db_properties = ( self.session.query(DBProperty) .filter_by(collection_path=self.path).all()) for prop in db_properties: properties[prop.key] = prop.value old_properties = properties.copy() yield properties # On exit if self._db_collection and old_properties != properties: for prop in db_properties: self.session.delete(prop) for key, value in properties.items(): prop = DBProperty() prop.key = key prop.value = value prop.collection_path = self.path self.session.add(prop) @property def items(self): return self._query( (ical.Event, ical.Todo, ical.Journal, ical.Card, ical.Timezone)) @property def components(self): return self._query((ical.Event, ical.Todo, ical.Journal, ical.Card)) @property def events(self): return self._query((ical.Event,)) @property def todos(self): return self._query((ical.Todo,)) @property def journals(self): return self._query((ical.Journal,)) @property def timezones(self): return self._query((ical.Timezone,)) @property def cards(self): return self._query((ical.Card,)) def save(self): """Save the text into the collection. This method is not used for databases. """ Radicale-0.8/radicale/storage/filesystem.py000066400000000000000000000070551217002223200210310ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Filesystem storage backend. """ import codecs import os import posixpath import json import time from contextlib import contextmanager from .. import config, ical FOLDER = os.path.expanduser(config.get("storage", "filesystem_folder")) # This function overrides the builtin ``open`` function for this module # pylint: disable=W0622 def open(path, mode="r"): """Open a file at ``path`` with encoding set in the configuration.""" abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) return codecs.open(abs_path, mode, config.get("encoding", "stock")) # pylint: enable=W0622 class Collection(ical.Collection): """Collection stored in a flat ical file.""" @property def _path(self): """Absolute path of the file at local ``path``.""" return os.path.join(FOLDER, self.path.replace("/", os.sep)) @property def _props_path(self): """Absolute path of the file storing the collection properties.""" return self._path + ".props" def _create_dirs(self): """Create folder storing the collection if absent.""" if not os.path.exists(os.path.dirname(self._path)): os.makedirs(os.path.dirname(self._path)) def save(self, text): self._create_dirs() open(self._path, "w").write(text) def delete(self): os.remove(self._path) os.remove(self._props_path) @property def text(self): try: return open(self._path).read() except IOError: return "" @classmethod def children(cls, path): abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) _, directories, files = next(os.walk(abs_path)) for filename in directories + files: rel_filename = posixpath.join(path, filename) if cls.is_node(rel_filename) or cls.is_leaf(rel_filename): yield cls(rel_filename) @classmethod def is_node(cls, path): abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) return os.path.isdir(abs_path) @classmethod def is_leaf(cls, path): abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) return os.path.isfile(abs_path) and not abs_path.endswith(".props") @property def last_modified(self): modification_time = time.gmtime(os.path.getmtime(self._path)) return time.strftime("%a, %d %b %Y %H:%M:%S +0000", modification_time) @property @contextmanager def props(self): # On enter properties = {} if os.path.exists(self._props_path): with open(self._props_path) as prop_file: properties.update(json.load(prop_file)) yield properties # On exit self._create_dirs() with open(self._props_path, "w") as prop_file: json.dump(properties, prop_file) Radicale-0.8/radicale/xmlutils.py000066400000000000000000000443351217002223200170640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ XML and iCal requests manager. Note that all these functions need to receive unicode objects for full iCal requests (PUT) and string objects with charset correctly defined in them for XML requests (all but PUT). """ try: from collections import OrderedDict except ImportError: # Python 2.6 has no OrderedDict, use a dict instead OrderedDict = dict # pylint: disable=C0103 import re import xml.etree.ElementTree as ET from . import client, config, ical NAMESPACES = { "C": "urn:ietf:params:xml:ns:caldav", "CR": "urn:ietf:params:xml:ns:carddav", "D": "DAV:", "CS": "http://calendarserver.org/ns/", "ICAL": "http://apple.com/ns/ical/", "ME": "http://me.com/_namespace/"} NAMESPACES_REV = {} for short, url in NAMESPACES.items(): NAMESPACES_REV[url] = short if hasattr(ET, "register_namespace"): # Register namespaces cleanly with Python 2.7+ and 3.2+ ... ET.register_namespace("" if short == "D" else short, url) else: # ... and badly with Python 2.6 and 3.1 ET._namespace_map[url] = short # pylint: disable=W0212 CLARK_TAG_REGEX = re.compile(r""" { # { (?P[^}]*) # namespace URL } # } (?P.*) # short tag name """, re.VERBOSE) def _pretty_xml(element, level=0): """Indent an ElementTree ``element`` and its children.""" i = "\n" + level * " " if len(element): if not element.text or not element.text.strip(): element.text = i + " " if not element.tail or not element.tail.strip(): element.tail = i for sub_element in element: _pretty_xml(sub_element, level + 1) # ``sub_element`` is always defined as len(element) > 0 # pylint: disable=W0631 if not sub_element.tail or not sub_element.tail.strip(): sub_element.tail = i # pylint: enable=W0631 else: if level and (not element.tail or not element.tail.strip()): element.tail = i if not level: output_encoding = config.get("encoding", "request") return ('\n' + ET.tostring( element, "utf-8").decode("utf-8")).encode(output_encoding) def _tag(short_name, local): """Get XML Clark notation {uri(``short_name``)}``local``.""" return "{%s}%s" % (NAMESPACES[short_name], local) def _tag_from_clark(name): """Get a human-readable variant of the XML Clark notation tag ``name``. For a given name using the XML Clark notation, return a human-readable variant of the tag name for known namespaces. Otherwise, return the name as is. """ match = CLARK_TAG_REGEX.match(name) if match and match.group("namespace") in NAMESPACES_REV: args = { "ns": NAMESPACES_REV[match.group("namespace")], "tag": match.group("tag")} return "%(ns)s:%(tag)s" % args return name def _response(code): """Return full W3C names from HTTP status codes.""" return "HTTP/1.1 %i %s" % (code, client.responses[code]) def _href(href): """Return prefixed href.""" return "%s%s" % (config.get("server", "base_prefix"), href.lstrip("/")) def name_from_path(path, collection): """Return Radicale item name from ``path``.""" collection_parts = collection.path.strip("/").split("/") path_parts = path.strip("/").split("/") if (len(path_parts) - len(collection_parts)): return path_parts[-1] def props_from_request(root, actions=("set", "remove")): """Return a list of properties as a dictionary.""" result = OrderedDict() if not hasattr(root, "tag"): root = ET.fromstring(root.encode("utf8")) for action in actions: action_element = root.find(_tag("D", action)) if action_element is not None: break else: action_element = root prop_element = action_element.find(_tag("D", "prop")) if prop_element is not None: for prop in prop_element: if prop.tag == _tag("D", "resourcetype"): for resource_type in prop: if resource_type.tag == _tag("C", "calendar"): result["tag"] = "VCALENDAR" break elif resource_type.tag == _tag("CR", "addressbook"): result["tag"] = "VADDRESSBOOK" break elif prop.tag == _tag("C", "supported-calendar-component-set"): result[_tag_from_clark(prop.tag)] = ",".join( supported_comp.attrib["name"] for supported_comp in prop if supported_comp.tag == _tag("C", "comp")) else: result[_tag_from_clark(prop.tag)] = prop.text return result def delete(path, collection): """Read and answer DELETE requests. Read rfc4918-9.6 for info. """ # Reading request if collection.path == path.strip("/"): # Delete the whole collection collection.delete() else: # Remove an item from the collection collection.remove(name_from_path(path, collection)) # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) response = ET.Element(_tag("D", "response")) multistatus.append(response) href = ET.Element(_tag("D", "href")) href.text = _href(path) response.append(href) status = ET.Element(_tag("D", "status")) status.text = _response(200) response.append(status) return _pretty_xml(multistatus) def propfind(path, xml_request, collections, user=None): """Read and answer PROPFIND requests. Read rfc4918-9.1 for info. The collections parameter is a list of collections that are to be included in the output. Rights checking has to be done by the caller. """ # Reading request if xml_request: root = ET.fromstring(xml_request.encode("utf8")) props = [prop.tag for prop in root.find(_tag("D", "prop"))] else: props = [_tag("D", "getcontenttype"), _tag("D", "resourcetype"), _tag("D", "displayname"), _tag("D", "owner"), _tag("D", "getetag"), _tag("CS", "getctag")] # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) for collection in collections: response = _propfind_response(path, collection, props, user) multistatus.append(response) return _pretty_xml(multistatus) def _propfind_response(path, item, props, user): """Build and return a PROPFIND response.""" is_collection = isinstance(item, ical.Collection) if is_collection: with item.props as properties: collection_props = properties response = ET.Element(_tag("D", "response")) href = ET.Element(_tag("D", "href")) uri = item.url if is_collection else "%s/%s" % (path, item.name) href.text = _href(uri.replace("//", "/")) response.append(href) propstat404 = ET.Element(_tag("D", "propstat")) propstat200 = ET.Element(_tag("D", "propstat")) response.append(propstat200) prop200 = ET.Element(_tag("D", "prop")) propstat200.append(prop200) prop404 = ET.Element(_tag("D", "prop")) propstat404.append(prop404) for tag in props: element = ET.Element(tag) is404 = False if tag == _tag("D", "getetag"): element.text = item.etag elif tag == _tag("D", "principal-URL"): tag = ET.Element(_tag("D", "href")) tag.text = _href(path) element.append(tag) elif tag in (_tag("D", "principal-collection-set"), _tag("C", "calendar-user-address-set"), _tag("CR", "addressbook-home-set"), _tag("C", "calendar-home-set")): tag = ET.Element(_tag("D", "href")) tag.text = _href(path) element.append(tag) elif tag == _tag("C", "supported-calendar-component-set"): # This is not a Todo # pylint: disable=W0511 human_tag = _tag_from_clark(tag) if is_collection and human_tag in collection_props: # TODO: what do we have to do if it's not a collection? components = collection_props[human_tag].split(",") else: components = ("VTODO", "VEVENT", "VJOURNAL") for component in components: comp = ET.Element(_tag("C", "comp")) comp.set("name", component) element.append(comp) # pylint: enable=W0511 elif tag == _tag("D", "current-user-principal") and user: tag = ET.Element(_tag("D", "href")) tag.text = _href("/%s/" % user) element.append(tag) elif tag == _tag("D", "current-user-privilege-set"): privilege = ET.Element(_tag("D", "privilege")) privilege.append(ET.Element(_tag("D", "all"))) privilege.append(ET.Element(_tag("D", "read"))) privilege.append(ET.Element(_tag("D", "write"))) privilege.append(ET.Element(_tag("D", "write-properties"))) privilege.append(ET.Element(_tag("D", "write-content"))) element.append(privilege) elif tag == _tag("D", "supported-report-set"): for report_name in ( "principal-property-search", "sync-collection", "expand-property", "principal-search-property-set"): supported = ET.Element(_tag("D", "supported-report")) report_tag = ET.Element(_tag("D", "report")) report_tag.text = report_name supported.append(report_tag) element.append(supported) elif is_collection: if tag == _tag("D", "getcontenttype"): element.text = item.mimetype elif tag == _tag("D", "resourcetype"): if item.is_principal: tag = ET.Element(_tag("D", "principal")) element.append(tag) if item.is_leaf(item.path) or ( not item.exists and item.resource_type): # 2nd case happens when the collection is not stored yet, # but the resource type is guessed if item.resource_type == "addressbook": tag = ET.Element(_tag("CR", item.resource_type)) else: tag = ET.Element(_tag("C", item.resource_type)) element.append(tag) tag = ET.Element(_tag("D", "collection")) element.append(tag) elif tag == _tag("D", "owner") and item.owner_url: element.text = item.owner_url elif tag == _tag("CS", "getctag"): element.text = item.etag elif tag == _tag("C", "calendar-timezone"): element.text = ical.serialize( item.tag, item.headers, item.timezones) elif tag == _tag("D", "displayname"): element.text = item.name else: human_tag = _tag_from_clark(tag) if human_tag in collection_props: element.text = collection_props[human_tag] else: is404 = True # Not for collections elif tag == _tag("D", "getcontenttype"): element.text = "%s; component=%s" % ( item.mimetype, item.tag.lower()) elif tag == _tag("D", "resourcetype"): # resourcetype must be returned empty for non-collection elements pass else: is404 = True if is404: prop404.append(element) else: prop200.append(element) status200 = ET.Element(_tag("D", "status")) status200.text = _response(200) propstat200.append(status200) status404 = ET.Element(_tag("D", "status")) status404.text = _response(404) propstat404.append(status404) if len(prop404): response.append(propstat404) return response def _add_propstat_to(element, tag, status_number): """Add a PROPSTAT response structure to an element. The PROPSTAT answer structure is defined in rfc4918-9.1. It is added to the given ``element``, for the following ``tag`` with the given ``status_number``. """ propstat = ET.Element(_tag("D", "propstat")) element.append(propstat) prop = ET.Element(_tag("D", "prop")) propstat.append(prop) if "{" in tag: clark_tag = tag else: clark_tag = _tag(*tag.split(":", 1)) prop_tag = ET.Element(clark_tag) prop.append(prop_tag) status = ET.Element(_tag("D", "status")) status.text = _response(status_number) propstat.append(status) def proppatch(path, xml_request, collection): """Read and answer PROPPATCH requests. Read rfc4918-9.2 for info. """ # Reading request root = ET.fromstring(xml_request.encode("utf8")) props_to_set = props_from_request(root, actions=("set",)) props_to_remove = props_from_request(root, actions=("remove",)) # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) response = ET.Element(_tag("D", "response")) multistatus.append(response) href = ET.Element(_tag("D", "href")) href.text = _href(path) response.append(href) with collection.props as collection_props: for short_name, value in props_to_set.items(): if short_name.split(":")[-1] == "calendar-timezone": collection.replace(None, value) collection_props[short_name] = value _add_propstat_to(response, short_name, 200) for short_name in props_to_remove: try: del collection_props[short_name] except KeyError: _add_propstat_to(response, short_name, 412) else: _add_propstat_to(response, short_name, 200) return _pretty_xml(multistatus) def put(path, ical_request, collection): """Read PUT requests.""" name = name_from_path(path, collection) if name in (item.name for item in collection.items): # PUT is modifying an existing item collection.replace(name, ical_request) else: # PUT is adding a new item collection.append(name, ical_request) def report(path, xml_request, collection): """Read and answer REPORT requests. Read rfc3253-3.6 for info. """ # Reading request root = ET.fromstring(xml_request.encode("utf8")) prop_element = root.find(_tag("D", "prop")) props = [prop.tag for prop in prop_element] if collection: if root.tag in (_tag("C", "calendar-multiget"), _tag("CR", "addressbook-multiget")): # Read rfc4791-7.9 for info base_prefix = config.get("server", "base_prefix") hreferences = set( href_element.text[len(base_prefix):] for href_element in root.findall(_tag("D", "href")) if href_element.text.startswith(base_prefix)) else: hreferences = (path,) # TODO: handle other filters # TODO: handle the nested comp-filters correctly # Read rfc4791-9.7.1 for info tag_filters = set( element.get("name") for element in root.findall(".//%s" % _tag("C", "comp-filter"))) else: hreferences = () tag_filters = None # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) collection_tag = collection.tag collection_items = collection.items collection_headers = collection.headers collection_timezones = collection.timezones for hreference in hreferences: # Check if the reference is an item or a collection name = name_from_path(hreference, collection) if name: # Reference is an item path = "/".join(hreference.split("/")[:-1]) + "/" items = (item for item in collection_items if item.name == name) else: # Reference is a collection path = hreference items = collection.components for item in items: if tag_filters and item.tag not in tag_filters: continue response = ET.Element(_tag("D", "response")) multistatus.append(response) href = ET.Element(_tag("D", "href")) href.text = _href("%s/%s" % (path.rstrip("/"), item.name)) response.append(href) propstat = ET.Element(_tag("D", "propstat")) response.append(propstat) prop = ET.Element(_tag("D", "prop")) propstat.append(prop) for tag in props: element = ET.Element(tag) if tag == _tag("D", "getetag"): element.text = item.etag elif tag == _tag("D", "getcontenttype"): element.text = "%s; component=%s" % ( item.mimetype, item.tag.lower()) elif tag in (_tag("C", "calendar-data"), _tag("CR", "address-data")): if isinstance(item, ical.Component): element.text = ical.serialize( collection_tag, collection_headers, collection_timezones + [item]) prop.append(element) status = ET.Element(_tag("D", "status")) status.text = _response(200) propstat.append(status) return _pretty_xml(multistatus) Radicale-0.8/schema.sql000066400000000000000000000017041217002223200150370ustar00rootroot00000000000000-- This is the database schema for PostgreSQL. begin; create table collection ( path varchar primary key not null, parent_path varchar references collection (path)); create table item ( name varchar primary key not null, tag varchar not null, collection_path varchar references collection (path) not null); create table header ( key varchar not null, value varchar not null, collection_path varchar references collection (path) not null, primary key (key, collection_path)); create table line ( key varchar not null, value varchar not null, item_name varchar references item (name) not null, timestamp timestamp not null, primary key (key, item_name)); create table property ( key varchar not null, value varchar not null, collection_path varchar references collection (path) not null, primary key (key, collection_path)); commit; Radicale-0.8/setup.py000077500000000000000000000056721217002223200146030ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2009-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale CalDAV and CardDAV server ================================== The Radicale Project is a CalDAV (calendar) and CardDAV (contact) server. It aims to be a light solution, easy to use, easy to install, easy to configure. As a consequence, it requires few software dependances and is pre-configured to work out-of-the-box. The Radicale Project runs on most of the UNIX-like platforms (Linux, BSD, MacOS X) and Windows. It is known to work with Evolution, Lightning, iPhone and Android clients. It is free and open-source software, released under GPL version 3. For further information, please visit the `Radicale Website `_. """ from distutils.core import setup import radicale # When the version is updated, ``radicale.VERSION`` must be modified. # A new section in the ``NEWS`` file must be added too. setup( name="Radicale", version=radicale.VERSION, description="CalDAV and CardDAV Server", long_description=__doc__, author="Guillaume Ayoub", author_email="guillaume.ayoub@kozea.fr", url="http://www.radicale.org/", download_url=("http://pypi.python.org/packages/source/R/Radicale/" "Radicale-%s.tar.gz" % radicale.VERSION), license="GNU GPL v3", platforms="Any", packages=[ "radicale", "radicale.auth", "radicale.rights", "radicale.storage"], provides=["radicale"], scripts=["bin/radicale"], keywords=["calendar", "addressbook", "CalDAV", "CardDAV"], classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", "License :: OSI Approved :: GNU General Public License (GPL)", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Topic :: Office/Business :: Groupware"]) Radicale-0.8/tests/000077500000000000000000000000001217002223200142165ustar00rootroot00000000000000Radicale-0.8/tests/__init__.py000066400000000000000000000036201217002223200163300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Tests for Radicale. """ import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import radicale class BaseTest(object): """Base class for tests.""" def setup(self): """Setup function for each test.""" self.application = radicale.Application() def teardown(self): """Teardown function for each test.""" def request(self, method, path, **args): """Send a request.""" self.application._status = None self.application._headers = None self.application._answer = None for key in args: args[key.upper()] = args[key] args["REQUEST_METHOD"] = method.upper() args["PATH_INFO"] = path self.application._answer = self.application(args, self.start_response) return ( int(self.application._status.split()[0]), dict(self.application._headers), self.application._answer[0].decode("utf-8")) def start_response(self, status, headers): """Put the response values into the current application.""" self.application._status = status self.application._headers = headers Radicale-0.8/tests/test_base.py000066400000000000000000000021131217002223200165360ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server # Copyright © 2012-2013 Guillaume Ayoub # # This library 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 library 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 Radicale. If not, see . """ Radicale tests with simple requests. """ from . import BaseTest class TestBaseRequests(BaseTest): """Tests with simple requests.""" def test_root(self): """Test a GET request at "/".""" status, headers, answer = self.request("GET", "/") assert status == 200 assert "Radicale works!" in answer Radicale-0.8/tox.ini000066400000000000000000000005161217002223200143710ustar00rootroot00000000000000[tox] envlist = py26, py27, py31, py32, py33 [base] deps = nose-cov pam requests [testenv] commands = nosetests [] [testenv:py26] deps = python-ldap {[base]deps} [testenv:py27] deps = python-ldap {[base]deps} [testenv:py31] deps = {[base]deps} [testenv:py32] deps = {[base]deps} [testenv:py33] deps = {[base]deps}