├── .gitignore ├── LICENSE ├── README.md ├── conf └── app.ini ├── go.mod ├── go.sum ├── logger └── log.go ├── main.go ├── models ├── device.go └── redis.go ├── plugins ├── active_sync │ ├── active_code.go │ ├── active_sync.go │ ├── sms.go │ └── user_info.go ├── owa │ └── owa.go └── web │ ├── public │ ├── css │ │ ├── common.css │ │ └── common.css.map │ ├── favicon.ico │ ├── i18n │ │ └── i18n.js │ ├── imgs │ │ ├── actived.png │ │ └── refused.png │ ├── js │ │ ├── jquery.i18n.fallbacks.js │ │ ├── jquery.i18n.js │ │ ├── jquery.i18n.messagestore.js │ │ ├── jquery.i18n │ │ │ └── 1.0.4 │ │ │ │ ├── jquery.i18n.js │ │ │ │ └── jquery.i18n.messagestore.js │ │ └── jquery │ │ │ └── 1.8.1 │ │ │ └── jquery.min.js │ └── scss │ │ ├── common.scss │ │ └── reset.scss │ ├── templates │ ├── activeSync.html │ └── deviceState.html │ └── web.go ├── settings └── settings.go ├── util ├── http_client.go ├── lib.go └── wbxml │ └── wbxml.go └── vars └── vars.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | certs/* 3 | 1.tgz 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Exchange_proxy 2 | 3 | Exchange_proxy是由go语言开发的Exchange安全代理,可以将内网的Exchange服务器的https服务安全地发布出去, 4 | 支持的功能如下: 5 | 6 | - WEB端增加OTP二次认证 7 | - 手机端增加设备激活绑定的功能 8 | - 屏蔽了PC端的EWS协议 9 | 10 | 在使用该系统前,需要确保有以下基础设施的接口,并完成与代理的对接: 11 | 12 | - OTP动态口令系统 13 | - 短信发送接口 14 | - 通过员工姓名查询员工手机号的接口 15 | 16 | 以上接口准备并对接完成后,正确配置conf/app.ini即可启动代理服务器了。 17 | 18 | ```ini 19 | [mail] 20 | hosts = mail.xiaomi.com,mail.sec.lu 21 | backend = https://10.10.10.10 22 | port = 443 23 | ssl = true 24 | cert = certs/ca.crt 25 | key = certs/ca.key 26 | ; debug level: Fatal, Error, Warn, Info, Debug 27 | debug_level = info 28 | 29 | [redis] 30 | host = 10.10.10.20 31 | port = 6379 32 | db = 0 33 | password = redis_passw0rd 34 | 35 | [otp] 36 | url = https://otp_api_url/chk_otp 37 | 38 | [sms] 39 | url = http://sms_api_url/api/send_sms 40 | header = X-SMS-Token 41 | key = token 42 | 43 | [user_info] 44 | user_phone = http://hr_api_url/findMobile 45 | active_url = https://mail.sec.lu/a/ 46 | 47 | ``` 48 | 49 | 配置文件说明: 50 | 51 | - mail节下的配置项是配置邮箱服务器本身的 52 | - hosts表示邮箱域名,支持配置多个用英文逗号分割的域名 53 | - backend表示邮箱服务器地址 54 | - ssl表示是否启用https,必须设为true 55 | - cert和key分别表示证书的公、私钥,与nginx的证书完全兼容 56 | - debug_level表示日志级别,默认为info级别 57 | 58 | - redis节表示redis服务器的配置 59 | - otp节为动态口令检测API的URL 60 | - sms节表示短信接口的API 61 | - user_info节下的user_phone表示查找手机的接口,active_url表示手机中激活连接的URL 62 | 63 | 设置好配置文件后,可通过`./main`直接启动代理服务器,如下图所示: 64 | ![](http://docs.xsec.io/images/mail_proxy/mail_proxy041.png) 65 | 66 | WEB通过外网访问WEB端时,要求必须输入正确的OTP口令才可以登录,如下图所示: 67 | ![](http://docs.xsec.io/images/mail_proxy/mail_proxy03.png) 68 | 69 | 通过手机端访问时,只有通过短信中的提示激活后,方可收发邮件,如下图所示: 70 | 71 | - 收到激活短信 72 | ![](http://docs.xsec.io/images/mail_proxy/mail_proxy04.png) 73 | 74 | - 激活确认页面 75 | 76 | ![](http://docs.xsec.io/images/mail_proxy/mail_proxy05.png) 77 | 78 | - 激活成功页面 79 | ![](http://docs.xsec.io/images/mail_proxy/mail_proxy06.png) 80 | 81 | 正式上线之前,最好提供相应的管理后台并与内网的管理系统对接,邮件代理管理后台提供以下功能: 82 | 83 | - 管理员可查看、修改每个用户的账户与设备状态 84 | - 管理员可查看每个设备的激活进程,方便故障排查 85 | - 用户也可自行管理自己的设备 86 | 87 | 设备数据保存在redis中,用go/python/php等语言都可以实现,我就不单独提供了。 88 | 89 | 代理系统的进程可以托管在supervisor或god中,部署了该系统后,可以解决邮件服务器手机端与WEB端的安全,目前的开源版本没有电脑端的安全代理功能,建议在PC端收发邮件时拨入VPN,或者在电脑中用BlueMail客户端收发邮件。 -------------------------------------------------------------------------------- /conf/app.ini: -------------------------------------------------------------------------------- 1 | [mail] 2 | hosts = mail.xiaomi.com,mail.sec.lu 3 | backend = https://10.10.10.10 4 | port = 443 5 | ssl = true 6 | cert = certs/ca.crt 7 | key = certs/ca.key 8 | ; debug level: Fatal, Error, Warn, Info, Debug 9 | debug_level = info 10 | 11 | [redis] 12 | host = 10.10.10.20 13 | port = 6379 14 | db = 0 15 | password = redis_passw0rd 16 | 17 | [otp] 18 | url = https://otp_api_url/chk_otp 19 | 20 | [sms] 21 | url = http://sms_api_url/api/send_sms 22 | header = X-SMS-Token 23 | key = token 24 | 25 | [user_info] 26 | user_phone = http://hr_api_url/findMobile 27 | active_url = https://mail.sec.lu 28 | 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module exchange_proxy 2 | 3 | require ( 4 | github.com/go-redis/redis v6.15.1+incompatible 5 | github.com/gorilla/websocket v1.4.0 // indirect 6 | github.com/magicmonty/activesync-go v0.0.0-20121203123100-40f879136bcb 7 | github.com/magicmonty/wbxml-go v0.0.0-20121123081138-2c61d93f134b 8 | github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47 // indirect 9 | github.com/mattn/go-colorable v0.1.1 // indirect 10 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 11 | github.com/onsi/ginkgo v1.7.0 // indirect 12 | github.com/onsi/gomega v1.4.3 // indirect 13 | github.com/sirupsen/logrus v1.3.0 14 | github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa // indirect 15 | github.com/toolkits/slice v0.0.0-20141116085117-e44a80af2484 16 | github.com/vulcand/oxy v0.0.0-20181130145254-c34b0c501e43 17 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 18 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd // indirect 19 | gopkg.in/ini.v1 v1.42.0 20 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/go-redis/redis v6.15.1+incompatible h1:BZ9s4/vHrIqwOb0OPtTQ5uABxETJ3NRuUNoSUurnkew= 6 | github.com/go-redis/redis v6.15.1+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 7 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 8 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 10 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 11 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 12 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 13 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 14 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 15 | github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= 16 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 17 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 18 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 19 | github.com/magicmonty/activesync-go v0.0.0-20121203123100-40f879136bcb h1:uDfElOCEXqYB1xPY5qxjJwJShKdSY5LiJMWfm6S5QVc= 20 | github.com/magicmonty/activesync-go v0.0.0-20121203123100-40f879136bcb/go.mod h1:pvQDeG1h7YU4W6x4xqPOHVLjs4UQ/Sk4X5WCrsjIcPM= 21 | github.com/magicmonty/wbxml-go v0.0.0-20121123081138-2c61d93f134b h1:dbUq4tyYWJbb+uusWCXag8wdf+fqg7hO0oSp8eC0Ypg= 22 | github.com/magicmonty/wbxml-go v0.0.0-20121123081138-2c61d93f134b/go.mod h1:k5Vtn0pkLSLO8fbQWknTKf03dbWHM29tcbEZ7+4RVlQ= 23 | github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47 h1:jlyJPTyctWqANbaxi/nXRrxX4WeeAGMPaHPj9XlO0Rw= 24 | github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4= 25 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 26 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 27 | github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= 28 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 29 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 30 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 31 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 32 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 33 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 34 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 35 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= 39 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 40 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 41 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 42 | github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa h1:E+gaaifzi2xF65PbDmuKI3PhLWY6G5opMLniFq8vmXA= 43 | github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa/go.mod h1:2RVY1rIf+2J2o/IM9+vPq9RzmHDSseB7FoXiSNIUsoU= 44 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 46 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 47 | github.com/toolkits/slice v0.0.0-20141116085117-e44a80af2484 h1:ERmNDajjCusGqV3uLhlHhRNpvQiGM1S6pd0RSe3LP+w= 48 | github.com/toolkits/slice v0.0.0-20141116085117-e44a80af2484/go.mod h1:2ZKxkgYEh//pN54EFSkJBak63sg0LxQQFjQmpEwxQx0= 49 | github.com/vulcand/oxy v0.0.0-20181130145254-c34b0c501e43 h1:2+LBGeYYE8JgE1UCKx+qRpoFyvF4NdGQpR8bFCCPfEw= 50 | github.com/vulcand/oxy v0.0.0-20181130145254-c34b0c501e43/go.mod h1:giFb8dicROVdV5W0HXlA5siMBLWKnVXZlkA4Y5ZIzrY= 51 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 52 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 53 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= 54 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 55 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 56 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= 57 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 58 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 59 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 61 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 63 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 65 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 67 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 71 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 72 | gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= 73 | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 74 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= 75 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 76 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 77 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 78 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 79 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 80 | -------------------------------------------------------------------------------- /logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/x-cray/logrus-prefixed-formatter" 6 | ) 7 | 8 | var ( 9 | Log *logrus.Entry 10 | ) 11 | 12 | func init() { 13 | logger := logrus.New() 14 | logger.Formatter = new(prefixed.TextFormatter) 15 | Log = logger.WithFields(logrus.Fields{"prefix": "exchange proxy"}) 16 | Log.Logger.SetLevel(logrus.InfoLevel) 17 | } 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "exchange_proxy/logger" 5 | "exchange_proxy/plugins/active_sync" 6 | "exchange_proxy/plugins/owa" 7 | "exchange_proxy/plugins/web" 8 | "exchange_proxy/vars" 9 | 10 | "fmt" 11 | "net/http" 12 | 13 | "github.com/toolkits/slice" 14 | ) 15 | 16 | func main() { 17 | mux := http.NewServeMux() 18 | staticFiles := http.FileServer(http.Dir("plugins/web/public")) 19 | mux.Handle("/static/", http.StripPrefix("/static/", staticFiles)) 20 | mux.HandleFunc("/", owa.OwaHandler(owa.OwaRedirect)) 21 | mux.HandleFunc("/owa/", owa.OwaHandler(owa.OwaRedirect)) 22 | mux.HandleFunc("/Microsoft-Server-ActiveSync", active_sync.ActiveSyncHandler(active_sync.SyncRedirect)) 23 | mux.HandleFunc("/a/", web.Activation) 24 | mux.HandleFunc("/a/activedevice", web.ActiveDevice) 25 | mux.HandleFunc("/a/ignoredevice", web.IgnoreDevice) 26 | 27 | mux.HandleFunc("/EWS/", web.NotFound) 28 | mux.HandleFunc("/ecp/", web.NotFound) 29 | mux.HandleFunc("/rpc/", web.NotFound) 30 | mux.HandleFunc("/Autodiscover/", web.NotFound) 31 | 32 | s := &http.Server{ 33 | Addr: fmt.Sprintf(":%v", vars.MailConfig.Port), 34 | Handler: mux, 35 | } 36 | 37 | go func() { 38 | listen80Port() 39 | }() 40 | 41 | var err error 42 | if vars.MailConfig.TLS { 43 | err = s.ListenAndServeTLS(vars.MailConfig.Cert, vars.MailConfig.Key) 44 | } else { 45 | err = s.ListenAndServe() 46 | } 47 | logger.Log.Println(err) 48 | } 49 | 50 | func listen80Port() { 51 | mux := http.NewServeMux() 52 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 53 | if slice.ContainsString(vars.MailConfig.Host, r.Host) { 54 | logger.Log.Debugf("redirect http://%v -> https://%v", r.Host, r.Host) 55 | w.Header().Set("Location", fmt.Sprintf("https://%v", r.Host)) 56 | w.WriteHeader(302) 57 | } 58 | }) 59 | 60 | s := &http.Server{ 61 | Addr: ":80", 62 | Handler: mux, 63 | } 64 | 65 | _ = s.ListenAndServe() 66 | } 67 | -------------------------------------------------------------------------------- /models/device.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "exchange_proxy/util/wbxml" 5 | "exchange_proxy/vars" 6 | 7 | "encoding/json" 8 | "fmt" 9 | "strconv" 10 | ) 11 | 12 | type ( 13 | // 移动设备信息 14 | Device struct { 15 | DeviceType string `json:"device_type"` 16 | DeviceId string `json:"device_id"` 17 | User string `json:"user"` 18 | State int `json:"state"` 19 | Time int64 `json:"time"` 20 | } 21 | UserPhone struct { 22 | Code int `json:"code"` 23 | Data string `json:"data"` 24 | } 25 | ) 26 | 27 | // 注册新设备,默认为未激活状态 28 | func NewDevice(user string, device Device) (err error) { 29 | InitRedis() 30 | v := make(map[string]interface{}) 31 | v["user_state"] = 0 32 | info, _ := json.Marshal(device) 33 | v[device.DeviceId] = string(info) 34 | key := fmt.Sprintf("iuser_%v", user) 35 | _, err = vars.RedisInstance.HMSet(key, v).Result() 36 | return err 37 | } 38 | 39 | // 新建用户,默认为激活状态 40 | func NewUser(user string) (err error) { 41 | InitRedis() 42 | key := fmt.Sprintf("iuser_%v", user) 43 | v := make(map[string]interface{}) 44 | v["user_state"] = 0 45 | _, err = vars.RedisInstance.HMSet(key, v).Result() 46 | return err 47 | } 48 | 49 | // 获取用户状态:激活与锁定 50 | func GetUserState(user string) (userState int, err error) { 51 | userState = -1 52 | InitRedis() 53 | key := fmt.Sprintf("iuser_%v", user) 54 | ret, err := vars.RedisInstance.HGet(key, "user_state").Result() 55 | userState, _ = strconv.Atoi(ret) 56 | return userState, err 57 | } 58 | 59 | // 获取设备状态:激活、未激活、锁定等 60 | func GetDeviceState(user, deviceId string) (state int) { 61 | state = -1 62 | InitRedis() 63 | key := fmt.Sprintf("iuser_%v", user) 64 | ret, err := vars.RedisInstance.HGet(key, deviceId).Result() 65 | if err == nil { 66 | var device Device 67 | err := json.Unmarshal([]byte(ret), &device) 68 | if err == nil { 69 | state = device.State 70 | } 71 | } 72 | return state 73 | } 74 | 75 | // 获取设备信息 76 | func GetDeviceInfo(user, deviceId string) (err error, deviceInfo Device) { 77 | InitRedis() 78 | key := fmt.Sprintf("iuser_%v", user) 79 | info, err := vars.RedisInstance.HGet(key, deviceId).Result() 80 | if err == nil { 81 | err = json.Unmarshal([]byte(info), &deviceInfo) 82 | } 83 | return err, deviceInfo 84 | } 85 | 86 | // 设置设备的状态:激活,锁定 87 | // ALLOW = 0 88 | // NEW = 1 89 | // LOCKED = 2 90 | // BLOCK = 3 91 | func SetDeviceState(user, deviceId string, state int) (err error) { 92 | InitRedis() 93 | key := fmt.Sprintf("iuser_%v", user) 94 | err, deviceInfo := GetDeviceInfo(user, deviceId) 95 | if err == nil { 96 | deviceInfo.State = state 97 | info, err := json.Marshal(deviceInfo) 98 | v := make(map[string]interface{}) 99 | v[deviceId] = string(info) 100 | if err == nil { 101 | _, err = vars.RedisInstance.HMSet(key, v).Result() 102 | } 103 | } 104 | return err 105 | } 106 | 107 | // 激活设备 108 | func ActiveDevice(user, deviceId string) (err error) { 109 | state := 0 110 | err = SetDeviceState(user, deviceId, state) 111 | return err 112 | } 113 | 114 | // 恢复设备 115 | func RestoreDevice(user, deviceId string) (err error) { 116 | state := 1 117 | err = SetDeviceState(user, deviceId, state) 118 | return err 119 | } 120 | 121 | // 锁定设备 122 | func LockDevice(user, deviceId string) (err error) { 123 | state := 2 124 | err = SetDeviceState(user, deviceId, state) 125 | return err 126 | } 127 | 128 | // 忽略设备 129 | func IgnoreDevice(user, deviceId string) (err error) { 130 | state := 3 131 | err = SetDeviceState(user, deviceId, state) 132 | return err 133 | } 134 | 135 | // 指令检测 136 | func CheckCmd(cmd string, cmdType map[string]bool) (result bool) { 137 | if cmdType[cmd] { 138 | result = true 139 | } 140 | return result 141 | } 142 | 143 | // 存储手机设备的信息 144 | func SetDeviceInfo(deviceId string, deviceInfo wbxml.DeviceInfo) (err error) { 145 | m := make(map[string]interface{}) 146 | deviceInfoStr, _ := json.Marshal(deviceInfo) 147 | m[deviceId] = string(deviceInfoStr) 148 | InitRedis() 149 | key := "DEVICE_INFO_XSEC_MAIL" 150 | _, err = vars.RedisInstance.HMSet(key, m).Result() 151 | return err 152 | } 153 | 154 | // 通过设备ID查询手机设备的信息 155 | func GetDeviceInfoByDeviceId(deviceId string) (deviceInfo wbxml.DeviceInfo, err error) { 156 | InitRedis() 157 | key := "DEVICE_INFO_XSEC_MAIL" 158 | deviceInfoStr, err := vars.RedisInstance.HGet(key, deviceId).Result() 159 | err = json.Unmarshal([]byte(deviceInfoStr), &deviceInfo) 160 | return deviceInfo, err 161 | } 162 | 163 | // 获取设备列表 164 | func GetDeviceList(username string) (devices []string, err error) { 165 | key := fmt.Sprintf("iuser_%v", username) 166 | InitRedis() 167 | devices, err = vars.RedisInstance.HVals(key).Result() 168 | return devices, err 169 | } 170 | 171 | // 获取设备数 172 | func GetDeviceNum(username string) (n int) { 173 | devices, err := GetDeviceList(username) 174 | if err == nil { 175 | n = len(devices) 176 | } 177 | return n 178 | } 179 | -------------------------------------------------------------------------------- /models/redis.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "exchange_proxy/logger" 8 | "exchange_proxy/vars" 9 | 10 | "github.com/go-redis/redis" 11 | ) 12 | 13 | func init() { 14 | InitRedis() 15 | } 16 | 17 | func InitRedis() () { 18 | var err error 19 | vars.RedisInstance, err = NewRedisClient(vars.RedisConf.Host, vars.RedisConf.Port, vars.RedisConf.Db, vars.RedisConf.Password) 20 | if err != nil { 21 | logger.Log.Errorf("connect redis failed, err: %v", err) 22 | } 23 | } 24 | 25 | func NewRedisClient(host string, port int, db int, password string) (*redis.Client, error) { 26 | client := redis.NewClient(&redis.Options{ 27 | Addr: fmt.Sprintf("%v:%v", host, port), 28 | Password: password, // no password set 29 | DB: db, // use default DB 30 | ReadTimeout: time.Minute, // set timeout value = 60 31 | }) 32 | 33 | _, err := client.Ping().Result() 34 | return client, err 35 | } 36 | -------------------------------------------------------------------------------- /plugins/active_sync/active_code.go: -------------------------------------------------------------------------------- 1 | package active_sync 2 | 3 | import ( 4 | "exchange_proxy/models" 5 | "exchange_proxy/util" 6 | "exchange_proxy/vars" 7 | "strings" 8 | 9 | "encoding/json" 10 | "fmt" 11 | "time" 12 | ) 13 | 14 | func DoActiveDevice(user, deviceId, deviceType string, device models.Device) { 15 | _, isActive := GetDeviceActiveFlag(user, deviceId) 16 | if !isActive { 17 | // 设置进入设备激活流程的FLAG 18 | SetDeviceActiveFlag(user, deviceId) 19 | // 获取用户手机号 20 | phone, err := GetUserPhone(vars.UserPhoneUrl, user) 21 | if err == nil { 22 | // 生成激活码 23 | _, code := SetActiveCode(user, deviceId, device) 24 | // 发送激活码 25 | smsStatus := SendCode(user, deviceId, deviceType, phone, code) 26 | // 发送成功的话删除已发送过的激活码 27 | if smsStatus { 28 | _ = DelActiveCodeFlag(user, deviceId) 29 | } 30 | } 31 | } else { 32 | // logger.Log.Infof("%v, 已经激活过,忽略请求", device) 33 | // 已经激活过,忽略请求 34 | } 35 | } 36 | 37 | // 获取设备激活标识 38 | func GetDeviceActiveFlag(username, deviceId string) (err error, result bool) { 39 | models.InitRedis() 40 | key := fmt.Sprintf("active_%v_%v", username, deviceId) 41 | v, err := vars.RedisInstance.Exists(key).Result() 42 | if v == 1 { 43 | result = true 44 | } 45 | // logger.Log.Infof("key:%v, v: %v, err: %v, result: %v", key, v, err, result) 46 | return err, result 47 | } 48 | 49 | // 设置设备激活标识 50 | func SetDeviceActiveFlag(username, deviceId string) { 51 | models.InitRedis() 52 | key := fmt.Sprintf("active_%v_%v", username, deviceId) 53 | _, _ = vars.RedisInstance.Set(key, deviceId, 60*time.Second).Result() 54 | } 55 | 56 | // 激活码是否生成过,生成过的话返回老激活码 57 | func GetActiveCodeFlag(username, deviceId string) (has bool, code string) { 58 | key := fmt.Sprintf("icodeF_%v_%v", username, deviceId) 59 | models.InitRedis() 60 | ret, err := vars.RedisInstance.Get(key).Result() 61 | 62 | if err == nil && ret != "" { 63 | has = true 64 | code = ret 65 | } 66 | return has, code 67 | } 68 | 69 | // 设置验证码的标识,判断是否生成过验证码,12小时后失效 70 | func SetActiveCodeFlag(username, deviceId, code string) { 71 | key := fmt.Sprintf("icodeF_%v_%v", username, deviceId) 72 | models.InitRedis() 73 | vars.RedisInstance.Set(key, code, 60*60*12*time.Second) 74 | } 75 | 76 | // 删除生成激活码的标识 77 | func DelActiveCodeFlag(username, deviceId string) (err error) { 78 | key := fmt.Sprintf("icodeF_%v_%v", username, deviceId) 79 | models.InitRedis() 80 | _, err = vars.RedisInstance.Del(key).Result() 81 | return err 82 | } 83 | 84 | // 设置激活码的值及超时时间,该函数不对外公开 85 | func setActiveCode(device models.Device, code string) (err error) { 86 | key := fmt.Sprintf("icode_%v", code) 87 | deviceStr, err := json.Marshal(device) 88 | v := fmt.Sprintf("%v-_-%v", device.User, string(deviceStr)) 89 | models.InitRedis() 90 | _, err = vars.RedisInstance.Set(key, v, 60*60*12*time.Second).Result() 91 | return err 92 | } 93 | 94 | // 激活码保存到redis中,有效期为8小时,不会多次生成 95 | func SetActiveCode(username, deviceId string, device models.Device) (err error, code string) { 96 | has, oldCode := GetActiveCodeFlag(username, deviceId) 97 | if has { 98 | code = oldCode 99 | err = setActiveCode(device, oldCode) 100 | } else { 101 | code = util.GenerateAtiveCode(username, deviceId) 102 | SetActiveCodeFlag(username, deviceId, code) 103 | err = setActiveCode(device, code) 104 | } 105 | return err, code 106 | } 107 | 108 | // 删除激活码 109 | func RemoveActiveCode(code string) (err error) { 110 | key := fmt.Sprintf("icode_%v", code) 111 | models.InitRedis() 112 | _, err = vars.RedisInstance.Del(key).Result() 113 | return err 114 | } 115 | 116 | // 获取激活码的值 117 | func getActiveCodeValue(code string) (err error, has bool, user string, deviceInfo models.Device) { 118 | key := fmt.Sprintf("icode_%v", code) 119 | models.InitRedis() 120 | c, err := vars.RedisInstance.Get(key).Result() 121 | if err == nil { 122 | t := strings.Split(c, "-_-") 123 | if len(t) == 2 { 124 | user = t[0] 125 | deviceStr := t[1] 126 | err = json.Unmarshal([]byte(deviceStr), &deviceInfo) 127 | if err == nil { 128 | has = true 129 | } 130 | } 131 | } 132 | return err, has, user, deviceInfo 133 | } 134 | 135 | // 检测激活码 136 | func CheckActiveCode(code string) (err error, has bool, user string, deviceInfo models.Device) { 137 | err, has, user, deviceInfo = getActiveCodeValue(code) 138 | return err, has, user, deviceInfo 139 | } 140 | 141 | // 激活时,验证激活码 142 | func VerifyActiveCode(code string) (result bool, user string, device models.Device) { 143 | err, has, user, deviceInfo := CheckActiveCode(code) 144 | if err == nil && has { 145 | status := deviceInfo.State 146 | if status == 1 { 147 | result = true 148 | } 149 | } 150 | return result, user, deviceInfo 151 | } 152 | 153 | // 在激活流程中使用过激活码后重置激活码的状态,设为已激活并在2小时后自动删除 154 | func ResetActiveCodeStatus(code, username string, device models.Device, state int) (result bool, err error) { 155 | models.InitRedis() 156 | key := fmt.Sprintf("icode_%v", code) 157 | device.State = state 158 | err = setActiveCode(device, code) 159 | if err == nil { 160 | result, err = vars.RedisInstance.Expire(key, 60*60*2*time.Second).Result() 161 | err = ResetSmsStatus(username, device.DeviceId) 162 | } 163 | return result, err 164 | } 165 | -------------------------------------------------------------------------------- /plugins/active_sync/active_sync.go: -------------------------------------------------------------------------------- 1 | package active_sync 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "exchange_proxy/logger" 12 | "exchange_proxy/models" 13 | "exchange_proxy/util/wbxml" 14 | "exchange_proxy/vars" 15 | 16 | "github.com/toolkits/slice" 17 | "github.com/vulcand/oxy/forward" 18 | "github.com/vulcand/oxy/testutils" 19 | ) 20 | 21 | func filterCmd(resp *http.Response) (err error) { 22 | var bodyBytes []byte 23 | bodyBytes, err = ioutil.ReadAll(resp.Request.Body) 24 | // 恢复Req.body的值给ParseForm函数使用 25 | resp.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 26 | values := resp.Request.URL.Query() 27 | if len(values) == 4 { 28 | cmd := values["Cmd"][0] 29 | mail := values["User"][0] 30 | deviceId := values["DeviceId"][0] 31 | user := "" 32 | // 取邮箱前缀 33 | u := strings.Split(mail, "@") 34 | if len(u) == 2 { 35 | user = u[0] 36 | } 37 | // 取域的\后面部分 38 | u1 := strings.Split(mail, "\\") 39 | if len(u1) == 2 { 40 | user = u1[1] 41 | } 42 | 43 | deviceState := models.GetDeviceState(user, deviceId) 44 | logger.Log.Infof("in filterCmd, user: %v, deviceId: %v, deviceState: %v", user, deviceId, deviceState) 45 | // 设备未激活时,过滤掉特定指令的返回值 46 | if deviceState != 0 { 47 | if models.CheckCmd(cmd, vars.ResponseCmds) { 48 | resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(""))) 49 | } 50 | } 51 | } 52 | 53 | // 恢复Req.body的值传到下一个处理器中 54 | resp.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 55 | return err 56 | } 57 | 58 | func SyncRedirect(w http.ResponseWriter, req *http.Request) { 59 | if slice.ContainsString(vars.MailConfig.Host, req.Host) { 60 | req.URL = testutils.ParseURI(vars.MailConfig.Backend) 61 | vars.FwdSync.ServeHTTP(w, req) 62 | } else { 63 | w.WriteHeader(444) 64 | } 65 | } 66 | 67 | func ActiveSyncHandler(h http.HandlerFunc) http.HandlerFunc { 68 | return func(w http.ResponseWriter, req *http.Request) { 69 | 70 | if vars.MailConfig.TLS { 71 | r := forward.RoundTripper( 72 | &http.Transport{ 73 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 74 | }, 75 | ) 76 | // 对手机设备的处理逻辑 77 | if strings.HasPrefix(req.RequestURI, "/Microsoft-Server-ActiveSync") { 78 | values := req.URL.Query() 79 | if len(values) > 0 { 80 | mail := values["User"][0] 81 | deviceId := values["DeviceId"][0] 82 | deviceType := values["DeviceType"][0] 83 | cmd := values["Cmd"][0] 84 | 85 | user := "" 86 | // 取邮箱前缀 87 | u := strings.Split(mail, "@") 88 | if len(u) == 2 { 89 | user = u[0] 90 | } 91 | // 取域的\后面部分 92 | u1 := strings.Split(mail, "\\") 93 | if len(u1) == 2 { 94 | user = u1[1] 95 | } 96 | 97 | device := models.Device{User: user, DeviceId: deviceId, DeviceType: deviceType, 98 | State: 1, Time: time.Now().Unix()} 99 | 100 | // 如果设备ID为0或为空,直接退出请求 101 | if len(deviceId) <= 1 { 102 | w.WriteHeader(444) 103 | } 104 | 105 | // 获取设备信息的详细信息 106 | var bodyBytes []byte 107 | bodyBytes, err := ioutil.ReadAll(req.Body) 108 | if err == nil { 109 | // 判断大小,否则可能会把邮件的附件传进来处理 110 | if len(bodyBytes) < 500 { 111 | deviceInfo, err := wbxml.Parse(bodyBytes) 112 | if err == nil && deviceInfo.Model != "" { 113 | logger.Log.Debugf("deviceInfo: %v, err: %v", deviceInfo, err) 114 | _ = models.SetDeviceInfo(deviceId, deviceInfo) 115 | } 116 | } 117 | } 118 | // 用完后恢复req.Body的值,否则之后的处理器不能再用了 119 | req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 120 | 121 | userState, _ := models.GetUserState(user) 122 | // logger.Log.Infof("user: %v, state: %v", user, userState) 123 | // 用户状态为1,表示已经锁定的账户,直接退出请求 124 | if userState == 1 { 125 | w.WriteHeader(444) 126 | } else if userState < 0 { 127 | _ = models.NewUser(user) 128 | } 129 | 130 | deviceState := models.GetDeviceState(user, deviceId) 131 | logger.Log.Debugf("user: %v, deviceId: %v, deviceState: %v, deviceType: %v, Cmd: %v", 132 | user, deviceId, deviceState, deviceType, cmd) 133 | if deviceState < 0 { 134 | // 设置不存在时,创建设备 135 | _ = models.NewDevice(user, device) 136 | // 未激活前需要过滤掉可以返回给客户端数据的指令及返回的数据 137 | vars.FwdSync, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), 138 | r, forward.ResponseModifier(filterCmd)) 139 | h(w, req) 140 | } else if deviceState == 0 { 141 | // 设备已激活就直接放行 142 | vars.FwdSync, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), r, forward.Stream(true)) 143 | h(w, req) 144 | } else if deviceState == 1 { 145 | // 设备未激活时,进入激活流程 146 | DoActiveDevice(user, deviceId, deviceType, device) 147 | 148 | // 未激活前需要过滤掉可以返回给客户端数据的指令及返回的数据 149 | vars.FwdSync, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), 150 | r, forward.ResponseModifier(filterCmd)) 151 | h(w, req) 152 | if models.CheckCmd(cmd, vars.RequestCmds) { 153 | w.WriteHeader(401) 154 | } 155 | } else { 156 | // 其他状态为锁定或阻止,不代理到后端,在代理层面直接返回 157 | w.WriteHeader(200) 158 | } 159 | } else { 160 | // 对于OPTIONS指令,暂时直接透传到后端 161 | vars.FwdSync, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), r) 162 | h(w, req) 163 | } 164 | 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /plugins/active_sync/sms.go: -------------------------------------------------------------------------------- 1 | package active_sync 2 | 3 | import ( 4 | "exchange_proxy/logger" 5 | "exchange_proxy/models" 6 | "exchange_proxy/vars" 7 | 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func SendCode(user, deviceId, deviceType, phone, code string) (status bool) { 17 | status = true 18 | key := fmt.Sprintf("sms_%v_%v_%v", user, deviceId, phone) 19 | models.InitRedis() 20 | ret, err := vars.RedisInstance.HMGet(key, "times", "send_time").Result() 21 | // logger.Log.Infof("key: %v, ret: %v, len(ret): %v, err: %v", key, ret, len(ret), err) 22 | now := time.Now().Unix() 23 | if err == nil && len(ret) == 2 { 24 | t, _ := ret[0].(string) 25 | times, _ := strconv.Atoi(t) 26 | st, _ := ret[1].(string) 27 | iSendTime, _ := strconv.Atoi(st) 28 | sendTime := int64(iSendTime) 29 | 30 | if times <= 5 && now-sendTime > 60*60*10 { 31 | content := generateSmsContent(user, deviceId, code, "", deviceType) 32 | _, ok := SendSmsAPI(user, phone, deviceId, code, content, "") 33 | status = ok 34 | times = times + 1 35 | sendTime = now 36 | v := make(map[string]interface{}) 37 | v["times"] = times 38 | v["send_time"] = sendTime 39 | _, _ = vars.RedisInstance.HMSet(key, v).Result() 40 | } else { 41 | status = false 42 | } 43 | } 44 | 45 | return status 46 | } 47 | 48 | // 重置短信状态,保证激活之后能再次收到短信,否则要等8小时之后了 49 | func ResetSmsStatus(user, deviceId string) (err error) { 50 | phone, err := GetUserPhone(vars.UserPhoneUrl, user) 51 | if err == nil { 52 | key := fmt.Sprintf("sms_%v_%v_%v", user, deviceId, phone) 53 | models.InitRedis() 54 | vars.RedisInstance.Del(key) 55 | } 56 | return err 57 | } 58 | 59 | // 发送短信 60 | func SendSmsAPI(username, phone, deviceId, code, content, srcIp string) (err error, result bool) { 61 | client := &http.Client{} 62 | postData := strings.NewReader(fmt.Sprintf("recipients=%s&content=%s", phone, content)) 63 | req, err := http.NewRequest("POST", vars.SmsApiUrl, postData) 64 | if err == nil { 65 | req.Header.Add(vars.SmsApiHeader, vars.SmsApiKey) 66 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 67 | 68 | resp, err := client.Do(req) 69 | if err == nil { 70 | result = true 71 | defer resp.Body.Close() 72 | ret, err := ioutil.ReadAll(resp.Body) 73 | logger.Log.Infof("user:%v, deviceId: %v, code: %v, sms send result: %v, err: %v", 74 | username, deviceId, code, strings.TrimSpace(string(ret)), err) 75 | } 76 | } 77 | return err, result 78 | } 79 | 80 | // 生成短信内容 81 | func generateSmsContent(username, deviceId, code, srcIp, deviceType string) (smsContent string) { 82 | // 获取用户手机设备信息 83 | deviceInfo, err := models.GetDeviceInfoByDeviceId(deviceId) 84 | if err == nil { 85 | phone := deviceInfo.PhoneNumber 86 | if deviceInfo.Model != "" { 87 | deviceType = deviceInfo.Model 88 | } 89 | if len(phone) > 5 { 90 | smsContent = fmt.Sprintf("您的邮箱 %v 正在一台新的设备(%v,手机号码为:%v)上登录,请点击链接查看详情并允许或拒绝,%v/a/?c=%v (此链接8小时后失效)", 91 | username, deviceType, phone, vars.ActiveUrl, code) 92 | } 93 | } else { 94 | smsContent = fmt.Sprintf("您的邮箱 %v 正在一台新的设备(%v)上登录,请点击链接查看详情并允许或拒绝,%v/a/?c=%v (此链接8小时后失效)", 95 | username, deviceType, vars.ActiveUrl, code) 96 | } 97 | 98 | return smsContent 99 | } 100 | 101 | -------------------------------------------------------------------------------- /plugins/active_sync/user_info.go: -------------------------------------------------------------------------------- 1 | package active_sync 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "exchange_proxy/models" 10 | ) 11 | 12 | // 获取手机号 13 | func GetUserPhone(url, user string) (phone string, err error) { 14 | var userPhone models.UserPhone 15 | resp, err := http.Get(fmt.Sprintf("%v?username=%v", url, user)) 16 | if err == nil { 17 | defer resp.Body.Close() 18 | body, err := ioutil.ReadAll(resp.Body) 19 | if err == nil { 20 | err = json.Unmarshal(body, &userPhone) 21 | if err == nil && userPhone.Code == 200 { 22 | phone = userPhone.Data 23 | } 24 | } 25 | } 26 | return phone, err 27 | } 28 | -------------------------------------------------------------------------------- /plugins/owa/owa.go: -------------------------------------------------------------------------------- 1 | package owa 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/toolkits/slice" 13 | "github.com/vulcand/oxy/forward" 14 | "github.com/vulcand/oxy/testutils" 15 | 16 | "exchange_proxy/logger" 17 | "exchange_proxy/util" 18 | "exchange_proxy/vars" 19 | ) 20 | 21 | func addOtp(resp *http.Response) error { 22 | b, err := ioutil.ReadAll(resp.Body) 23 | if err == nil { 24 | err = resp.Body.Close() 25 | 26 | oldHtml := "
" 27 | newHtml := "
动态口令:
" 28 | b = bytes.Replace(b, []byte(oldHtml), []byte(newHtml), -1) // replace html 29 | 30 | body := ioutil.NopCloser(bytes.NewReader(b)) 31 | resp.Body = body 32 | resp.ContentLength = int64(len(b)) 33 | resp.Header.Set("Content-Length", strconv.Itoa(len(b))) 34 | } 35 | return err 36 | } 37 | 38 | func OwaRedirect(w http.ResponseWriter, req *http.Request) { 39 | 40 | if slice.ContainsString(vars.MailConfig.Host, req.Host) { 41 | req.URL = testutils.ParseURI(vars.MailConfig.Backend) 42 | vars.FwdOWA.ServeHTTP(w, req) 43 | } else { 44 | u := fmt.Sprintf("https://%v//owa/auth/logon.aspx", vars.MailConfig.Host[0]) 45 | w.Header().Set("Location", u) 46 | w.WriteHeader(302) 47 | } 48 | } 49 | 50 | func OwaHandler(h http.HandlerFunc) http.HandlerFunc { 51 | return func(w http.ResponseWriter, req *http.Request) { 52 | 53 | // 内网用户不用验证OTP口令 54 | if strings.HasPrefix(req.RemoteAddr, "10.") || strings.HasPrefix(req.RemoteAddr, "192.") { 55 | r := forward.RoundTripper( 56 | &http.Transport{ 57 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 58 | }, 59 | ) 60 | vars.FwdOWA, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), r) 61 | h(w, req) 62 | } else { 63 | if vars.MailConfig.TLS { 64 | r := forward.RoundTripper( 65 | &http.Transport{ 66 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 67 | }, 68 | ) 69 | 70 | if req.Method == "GET" && strings.HasPrefix(req.RequestURI, "/owa/auth/logon.aspx") { 71 | vars.FwdOWA, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), 72 | r, forward.ResponseModifier(addOtp)) 73 | h(w, req) 74 | 75 | } else if req.Method == "POST" && strings.HasPrefix(req.RequestURI, "/owa/auth/logon.aspx") { 76 | var bodyBytes []byte 77 | if req.Body != nil { 78 | bodyBytes, _ = ioutil.ReadAll(req.Body) 79 | 80 | // 恢复Req.body的值给ParseForm函数使用 81 | req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 82 | _ = req.ParseForm() 83 | username := req.FormValue("username") 84 | customToken := req.FormValue("customToken") 85 | 86 | // 恢复Req.body的值传到下一个处理器中 87 | req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 88 | 89 | result, err := util.CheckToken(vars.OtpUrl, username, customToken) 90 | logger.Log.Printf("user: %v, token: %v, result: %v", username, customToken, result) 91 | if err == nil && result { 92 | vars.FwdOWA, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), r) 93 | h(w, req) 94 | } else { 95 | w.Header().Set("Location", "/owa/auth/logon.aspx") 96 | w.WriteHeader(302) 97 | } 98 | } 99 | } else { 100 | vars.FwdOWA, _ = forward.New(forward.PassHostHeader(true), forward.Logger(logger.Log.Logger), r) 101 | h(w, req) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /plugins/web/public/css/common.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0}a,a:hover{text-decoration:none;color:#09f}body{font-size:14px;font-family:arial, "Hiragino Sans GB", "Microsoft YaHei", "微軟正黑體", "儷黑 Pro", sans-serif;color:#000;background-color:#fff}.tac{text-align:center}.fl{float:left}.fr{float:right}ul{list-style:none}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}.container{padding:20px;max-width:400px;margin:auto}.mi_logo{width:48px;height:48px;margin:30px auto}.dear_user{font-size:16px;font-weight:bold;padding:10px 0}.help_phone{padding:10px 0}.modal{display:none;opacity:0;position:fixed;z-index:999;left:0;top:0;height:100%;width:100%;transition:opacity .5s}.allow_btn{margin-top:60px}.modal.visible{display:block;opacity:1}.modal_mask{position:absolute;left:0;top:0;width:100%;height:100%;opacity:.4;filter:alpha(opacity=40);background-color:black}.modal_box{text-align:center;position:absolute;bottom:-30px;width:90%;left:50%;box-shadow:0 1px 8px rgba(128,128,128,0.3);border:1px solid #d1d1d1;border-radius:10px;overflow:hidden;background-color:white;transition:width .5s ease, height .5s ease;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);-webkit-transform:translate(-50%, -50%, 0);transform:translate(-50%, -50%, 0);_top:0;_left:0;_margin-left:0;_margin-top:0;max-width:400px}.modal_body{margin:20px;text-align:left}.modal_footer{display:flex;border-top:1px solid #f8f8f8}.modal_btn{flex:1;padding:15px 0}.modal_btn_ok{color:#ff6700;border-left:1px solid #f8f8f8}.btn{margin-bottom:20px;text-align:center;padding:12px 0;border-radius:6px}.btn_ok{background-color:#ff6700;color:#fff}.btn_no{background-color:#fff;color:#999;border:1px solid #d3d3d3}.state{margin-top:30px}.state img{width:100px;height:100px;margin-top:40px}.state dd{font-size:30px;margin:20px 0}.device_list{margin:20px 0 20px 30px;list-style-type:disc}.device_list.gray{width:220px;color:#999;margin:40px auto 0 auto}.caution{margin:30px 0}.caution .line{width:100%;margin-bottom:-10px;border-bottom:1px solid #f0f0f0}.caution .txt{color:#999;background-color:#fff;padding:5px 10px}.tips{margin:20px 0}.icon_gter{width:10%;height:16px;display:block;position:relative}.icon_gter:after{content:"";width:12px;height:12px;border-width:1px;border-style:solid;border-color:transparent transparent rgba(0,0,0,0.3) rgba(0,0,0,0.3);-webkit-transform:rotate(-135deg);transform:rotate(-135deg);position:absolute;left:5px;top:4px}.icon_lser{width:10%;height:16px;display:block;position:relative}.icon_lser:after{content:"";width:12px;height:12px;border-width:1px;border-style:solid;border-color:transparent transparent rgba(0,0,0,0.3) rgba(0,0,0,0.3);-webkit-transform:rotate(45deg);transform:rotate(45deg);position:absolute;left:5px;top:4px}.active_tips{text-align:center}.more_tips .tips{height:60px;overflow:auto}.more_sec_info{font-size:12px}.lang-select-list{height:20px;line-height:20px;text-align:center}.lang-select-list li{font-size:14px;display:inline-block}.lang-select-list li a{padding:10px;color:#999}.lang-select-list li a.current{color:#ef5b00}.outlook_tips{color:#f66;font-size:12px;padding-bottom:20px;display:none} 2 | /*# sourceMappingURL=common.css.map */ 3 | -------------------------------------------------------------------------------- /plugins/web/public/css/common.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "CAAE,AAAE,EACA,KAAM,EAAE,AAAC,EACT,MAAO,EAAE,AAAC,EAGZ,QACQ,EACN,cAAe,EAAE,GAAI,EACrB,IAAK,EAAE,GAAI,EAGb,GAAK,EACH,QAAS,EAAE,GAAI,EACf,UAAW,EAAE,0EAA2E,EACxF,IAAK,EAAE,GAAI,EACX,eAAgB,EAAE,GAAI,EAGxB,GAAK,EACH,SAAU,EAAE,KAAM,EAGpB,EAAI,EACF,IAAK,EAAE,GAAI,EAGb,EAAI,EACF,IAAK,EAAE,IAAK,EAGd,CAAG,EACD,SAAU,EAAE,GAAI,EAIhB,+BACQ,EACN,MAAO,EAAE,EAAG,EACZ,MAAO,EAAE,IAAK,EAEhB,cAAQ,EACN,IAAK,EAAE,GAAI,ECvCjB,SAAW,EACT,MAAO,EAAE,GAAI,EACb,QAAS,EAAE,IAAK,EAChB,KAAM,EAAE,GAAI,EAGd,OAAS,EACP,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,GAAI,EACZ,KAAM,EAAE,QAAS,EAGnB,SAAW,EACT,QAAS,EAAE,GAAI,EACf,UAAW,EAAE,GAAI,EACjB,MAAO,EAAE,KAAM,EAGjB,UAAY,EACV,MAAO,EAAE,KAAM,EAGjB,KAAO,EACL,MAAO,EAAE,GAAI,EACb,MAAO,EAAE,AAAC,EACV,OAAQ,EAAE,IAAK,EACf,MAAO,EAAE,EAAG,EACZ,GAAI,EAAE,AAAC,EACP,EAAG,EAAE,AAAC,EACN,KAAM,EAAE,GAAI,EACZ,IAAK,EAAE,GAAI,EACX,SAAU,EAAE,UAAW,EAGzB,SAAW,EACT,SAAU,EAAE,GAAI,EAGlB,aAAe,EACb,MAAO,EAAE,IAAK,EACd,MAAO,EAAE,AAAC,EAGZ,UAAY,EACV,OAAQ,EAAE,OAAQ,EAClB,GAAI,EAAE,AAAC,EACP,EAAG,EAAE,AAAC,EACN,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,GAAI,EACZ,MAAO,EAAE,CAAE,EACX,KAAM,EAAE,gBAAiB,EACzB,eAAgB,EAAE,IAAK,EAGzB,SAAW,EACT,SAAU,EAAE,KAAM,EAClB,OAAQ,EAAE,OAAQ,EAClB,KAAM,EAAE,IAAK,EACb,IAAK,EAAE,EAAG,EACV,GAAI,EAAE,EAAG,EACT,SAAU,EAAE,8BAAkC,EAC9C,KAAM,EAAE,gBAAiB,EACzB,YAAa,EAAE,GAAI,EACnB,OAAQ,EAAE,KAAM,EAChB,eAAgB,EAAE,IAAK,EACvB,SAAU,EAAE,8BAA+B,EAC3C,gBAAiB,EAAE,oBAAqB,EACxC,QAAS,EAAE,oBAAqB,EAChC,gBAAiB,EAAE,uBAAwB,EAC3C,QAAS,EAAE,uBAAwB,EACnC,GAAI,EAAE,AAAC,EACP,IAAK,EAAE,AAAC,EACR,WAAY,EAAE,AAAC,EACf,UAAW,EAAE,AAAC,EACd,QAAS,EAAE,IAAK,EAGlB,UAAY,EACV,KAAM,EAAE,GAAI,EACZ,SAAU,EAAE,GAAI,EAGlB,YAAc,EACZ,MAAO,EAAE,GAAI,EACb,SAAU,EAAE,gBAAiB,EAG/B,SAAW,EACT,GAAI,EAAE,AAAC,EACP,MAAO,EAAE,KAAM,EAGjB,YAAc,EACZ,IAAK,EAAE,MAAO,EACd,UAAW,EAAE,gBAAiB,EAGhC,GAAK,EACH,YAAa,EAAE,GAAI,EACnB,SAAU,EAAE,KAAM,EAClB,MAAO,EAAE,KAAM,EACf,YAAa,EAAE,EAAG,EAGpB,MAAQ,EACN,eAAgB,EAAE,MAAO,EACzB,IAAK,EAAE,GAAI,EAGb,MAAQ,EACN,eAAgB,EAAE,GAAI,EACtB,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,gBAAiB,EAG3B,KAAO,EACL,SAAU,EAAE,GAAI,EAChB,SAAI,EACF,IAAK,EAAE,IAAK,EACZ,KAAM,EAAE,IAAK,EACb,SAAU,EAAE,GAAI,EAElB,QAAG,EACD,QAAS,EAAE,GAAI,EACf,KAAM,EAAE,KAAM,EAKlB,WAAa,EACX,KAAM,EAAE,eAAgB,EACxB,cAAe,EAAE,GAAI,EACrB,gBAAO,EACL,IAAK,EAAE,IAAK,EACZ,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,eAAgB,EAI5B,OAAS,EACP,KAAM,EAAE,KAAM,EACd,aAAM,EACJ,IAAK,EAAE,GAAI,EACX,YAAa,EAAE,IAAK,EACpB,YAAa,EAAE,gBAAiB,EAElC,YAAK,EACH,IAAK,EAAE,GAAI,EACX,eAAgB,EAAE,GAAI,EACtB,MAAO,EAAE,OAAQ,EAIrB,IAAM,EACJ,KAAM,EAAE,KAAM,EAGhB,SAAW,EACT,IAAK,EAAE,EAAG,EACV,KAAM,EAAE,GAAI,EACZ,MAAO,EAAE,IAAK,EACd,OAAQ,EAAE,OAAQ,EAClB,eAAQ,EACN,MAAO,EAAE,CAAE,EACX,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,GAAI,EACZ,WAAY,EAAE,EAAG,EACjB,WAAY,EAAE,IAAK,EACnB,WAAY,EAAE,sDAA6D,EAC3E,gBAAiB,EAAE,cAAe,EAClC,QAAS,EAAE,cAAe,EAC1B,OAAQ,EAAE,OAAQ,EAClB,GAAI,EAAE,EAAG,EACT,EAAG,EAAE,EAAG,EAIZ,SAAW,EACT,IAAK,EAAE,EAAG,EACV,KAAM,EAAE,GAAI,EACZ,MAAO,EAAE,IAAK,EACd,OAAQ,EAAE,OAAQ,EAClB,eAAQ,EACN,MAAO,EAAE,CAAE,EACX,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,GAAI,EACZ,WAAY,EAAE,EAAG,EACjB,WAAY,EAAE,IAAK,EACnB,WAAY,EAAE,sDAA6D,EAC3E,gBAAiB,EAAE,YAAa,EAChC,QAAS,EAAE,YAAa,EACxB,OAAQ,EAAE,OAAQ,EAClB,GAAI,EAAE,EAAG,EACT,EAAG,EAAE,EAAG,EAIZ,WAAa,EACX,SAAU,EAAE,KAAM,EAIlB,eAAM,EACJ,KAAM,EAAE,GAAI,EACZ,OAAQ,EAAE,GAAI,EAIlB,aAAe,EACb,QAAS,EAAE,GAAI,EAIjB,gBAAkB,EAChB,KAAM,EAAE,GAAI,EACZ,UAAW,EAAE,GAAI,EACjB,SAAU,EAAE,KAAM,EAClB,mBAAG,EACD,QAAS,EAAE,GAAI,EACf,MAAO,EAAE,WAAY,EACrB,qBAAE,EACA,MAAO,EAAE,GAAI,EACb,IAAK,EAAE,GAAI,EACX,6BAAU,EACR,IAAK,EAAE,MAAO,EAMtB,YAAc,EACZ,IAAK,EAAE,GAAI,EACX,QAAS,EAAE,GAAI,EACf,aAAc,EAAE,GAAI,EACpB,MAAO,EAAE,GAAI", 4 | "sources": ["../scss/reset.scss","../scss/common.scss"], 5 | "names": [], 6 | "file": "common.css" 7 | } -------------------------------------------------------------------------------- /plugins/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiSecurity/exchange_proxy/bd6604d4ddb2d4e3c1c730e0a9a1a8abc843e9af/plugins/web/public/favicon.ico -------------------------------------------------------------------------------- /plugins/web/public/i18n/i18n.js: -------------------------------------------------------------------------------- 1 | var txt = { 2 | cn: { 3 | "doc_title": "邮箱激活系统", 4 | "dear_user": "亲爱的:", 5 | "new_device": "您的邮箱 正在一台新的设备()上登录,详情如下", 6 | "device_name": "设备名称", 7 | "device_model": "设备型号", 8 | "device_type": "设备类型", 9 | "device_id": "设备ID", 10 | "imei_id": "IMEI", 11 | "phone_number":"手机号码", 12 | "ip_address": "新设备IP地址", 13 | "change_pwd": "如果这不是您本人操作,您的邮箱密码可能已经泄露,请修改密码并拒绝该请求。", 14 | "help_phone": "如需协助请联系IT部门", 15 | "outlook_tips": "您使用的是outlook客户端,用户名和密码会被保存到第三方微软公司的服务器中,存在安全风险,推荐使用手机自带的邮件客户端", 16 | "allow": " 允许", 17 | "reject": " 拒绝", 18 | "allow_confirm_tips": "确定后,将同步您的邮箱到该设备,如需禁用该设备,请登录内网帐号中心的“手机邮箱”中进行操作", 19 | "cancel": " 取消", 20 | "ok": "确定", 21 | "reject_manage_account": "拒绝后,将无法同步您的邮箱到该设备。如需重新激活该设备,请登录内网帐号中心的“手机邮箱”中进行操作", 22 | "device_actived": "设备已授权", 23 | "device_rejected": "设备已拒绝", 24 | "device_exceed": "设备数已超出限制", 25 | "page_not_found": "页面未找到", 26 | "link_invalid": "激活链接无效或已过期", 27 | "caution": "温馨提示", 28 | "auth_to_mis_manage": "已授权设备请登录到内网账号中心的“手机邮箱”中进行管理", 29 | "reject_to_mis_manage": "已拒绝设备请登录到内网账号中心的“手机邮箱”中进行管理", 30 | "to_mis_change_pwd": "如果这不是您本人操作,您的邮箱密码可能已泄露,并且有人正在尝试登陆您的邮箱,请立即点拒绝访问,并修改密码。", 31 | "devices_exceeds_the_limit": "已经激活的设备数超出了10台的限制,请中删除不再使用的设备", 32 | "more_sec_info": "更多安全知识,请访问 小米安全中心 ", 33 | "new_ip": "您的邮箱 正在使用()客户端进行同步邮件,详情如下", 34 | "client_type": "客户端类型", 35 | "device_ip": "移动设备IP地址", 36 | "client_ip": "客户端IP地址", 37 | "expire_tips": "该客户端 IP 有效期为 8 个小时,过期后需要重新进行授权", 38 | "allow_confirm_tips_ip": "确定后,将同步您的邮箱到该客户端,该客户端 IP 有效期为 8 个小时,如需禁用该 IP,请登录到“邮箱管理中心”中对 IP 进行管理", 39 | "reject_manage_account_ip": "拒绝后,将无法同步您的邮箱到该客户端。如需重新激活该 IP,请登录到“邮箱管理中心”中对 IP 进行管理", 40 | "ip_actived": "客户端 IP 已激活", 41 | "ip_rejected": "客户端 IP 已拒绝", 42 | "auth_to_mis_manage_ip": "已授权 IP 请登录到“邮箱管理中心”中对 IP 进行管理", 43 | "reject_to_mis_manage_ip": "已拒绝 IP 请登录到“邮箱管理中心”中对 IP 进行管理", 44 | "to_mis_change_pwd_ip": "如果该客户端 IP 不是您的 IP ,您的密码可能已经泄露,请立即修改密码" 45 | } 46 | }; 47 | $(function() { 48 | var $i18n = $.i18n(); 49 | $i18n.load(txt).done(function() { 50 | $('html').i18n(); 51 | $("[data-i18n^='[html]']").html(function() { 52 | return $.i18n($(this).attr('data-i18n').replace('\[html\]', '')); 53 | }); 54 | }) 55 | }) -------------------------------------------------------------------------------- /plugins/web/public/imgs/actived.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiSecurity/exchange_proxy/bd6604d4ddb2d4e3c1c730e0a9a1a8abc843e9af/plugins/web/public/imgs/actived.png -------------------------------------------------------------------------------- /plugins/web/public/imgs/refused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiSecurity/exchange_proxy/bd6604d4ddb2d4e3c1c730e0a9a1a8abc843e9af/plugins/web/public/imgs/refused.png -------------------------------------------------------------------------------- /plugins/web/public/js/jquery.i18n.fallbacks.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Internationalization library 3 | * 4 | * Copyright (C) 2012 Santhosh Thottingal 5 | * 6 | * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do anything special to 7 | * choose one license or the other and you don't have to notify anyone which license you are using. 8 | * You are free to use UniversalLanguageSelector in commercial projects as long as the copyright 9 | * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. 10 | * 11 | * @licence GNU General Public Licence 2.0 or later 12 | * @licence MIT License 13 | */ 14 | ( function ( $ ) { 15 | 'use strict'; 16 | 17 | $.i18n = $.i18n || {}; 18 | $.extend( $.i18n.fallbacks, { 19 | ab: [ 'ru' ], 20 | ace: [ 'id' ], 21 | aln: [ 'sq' ], 22 | // Not so standard - als is supposed to be Tosk Albanian, 23 | // but in Wikipedia it's used for a Germanic language. 24 | als: [ 'gsw', 'de' ], 25 | an: [ 'es' ], 26 | anp: [ 'hi' ], 27 | arn: [ 'es' ], 28 | arz: [ 'ar' ], 29 | av: [ 'ru' ], 30 | ay: [ 'es' ], 31 | ba: [ 'ru' ], 32 | bar: [ 'de' ], 33 | 'bat-smg': [ 'sgs', 'lt' ], 34 | bcc: [ 'fa' ], 35 | 'be-x-old': [ 'be-tarask' ], 36 | bh: [ 'bho' ], 37 | bjn: [ 'id' ], 38 | bm: [ 'fr' ], 39 | bpy: [ 'bn' ], 40 | bqi: [ 'fa' ], 41 | bug: [ 'id' ], 42 | 'cbk-zam': [ 'es' ], 43 | ce: [ 'ru' ], 44 | crh: [ 'crh-latn' ], 45 | 'crh-cyrl': [ 'ru' ], 46 | csb: [ 'pl' ], 47 | cv: [ 'ru' ], 48 | 'de-at': [ 'de' ], 49 | 'de-ch': [ 'de' ], 50 | 'de-formal': [ 'de' ], 51 | dsb: [ 'de' ], 52 | dtp: [ 'ms' ], 53 | egl: [ 'it' ], 54 | eml: [ 'it' ], 55 | ff: [ 'fr' ], 56 | fit: [ 'fi' ], 57 | 'fiu-vro': [ 'vro', 'et' ], 58 | frc: [ 'fr' ], 59 | frp: [ 'fr' ], 60 | frr: [ 'de' ], 61 | fur: [ 'it' ], 62 | gag: [ 'tr' ], 63 | gan: [ 'gan-hant', 'zh-hant', 'zh-hans' ], 64 | 'gan-hans': [ 'zh-hans' ], 65 | 'gan-hant': [ 'zh-hant', 'zh-hans' ], 66 | gl: [ 'pt' ], 67 | glk: [ 'fa' ], 68 | gn: [ 'es' ], 69 | gsw: [ 'de' ], 70 | hif: [ 'hif-latn' ], 71 | hsb: [ 'de' ], 72 | ht: [ 'fr' ], 73 | ii: [ 'zh-cn', 'zh-hans' ], 74 | inh: [ 'ru' ], 75 | iu: [ 'ike-cans' ], 76 | jut: [ 'da' ], 77 | jv: [ 'id' ], 78 | kaa: [ 'kk-latn', 'kk-cyrl' ], 79 | kbd: [ 'kbd-cyrl' ], 80 | khw: [ 'ur' ], 81 | kiu: [ 'tr' ], 82 | kk: [ 'kk-cyrl' ], 83 | 'kk-arab': [ 'kk-cyrl' ], 84 | 'kk-latn': [ 'kk-cyrl' ], 85 | 'kk-cn': [ 'kk-arab', 'kk-cyrl' ], 86 | 'kk-kz': [ 'kk-cyrl' ], 87 | 'kk-tr': [ 'kk-latn', 'kk-cyrl' ], 88 | kl: [ 'da' ], 89 | 'ko-kp': [ 'ko' ], 90 | koi: [ 'ru' ], 91 | krc: [ 'ru' ], 92 | ks: [ 'ks-arab' ], 93 | ksh: [ 'de' ], 94 | ku: [ 'ku-latn' ], 95 | 'ku-arab': [ 'ckb' ], 96 | kv: [ 'ru' ], 97 | lad: [ 'es' ], 98 | lb: [ 'de' ], 99 | lbe: [ 'ru' ], 100 | lez: [ 'ru' ], 101 | li: [ 'nl' ], 102 | lij: [ 'it' ], 103 | liv: [ 'et' ], 104 | lmo: [ 'it' ], 105 | ln: [ 'fr' ], 106 | ltg: [ 'lv' ], 107 | lzz: [ 'tr' ], 108 | mai: [ 'hi' ], 109 | 'map-bms': [ 'jv', 'id' ], 110 | mg: [ 'fr' ], 111 | mhr: [ 'ru' ], 112 | min: [ 'id' ], 113 | mo: [ 'ro' ], 114 | mrj: [ 'ru' ], 115 | mwl: [ 'pt' ], 116 | myv: [ 'ru' ], 117 | mzn: [ 'fa' ], 118 | nah: [ 'es' ], 119 | nap: [ 'it' ], 120 | nds: [ 'de' ], 121 | 'nds-nl': [ 'nl' ], 122 | 'nl-informal': [ 'nl' ], 123 | no: [ 'nb' ], 124 | os: [ 'ru' ], 125 | pcd: [ 'fr' ], 126 | pdc: [ 'de' ], 127 | pdt: [ 'de' ], 128 | pfl: [ 'de' ], 129 | pms: [ 'it' ], 130 | pt: [ 'pt-br' ], 131 | 'pt-br': [ 'pt' ], 132 | qu: [ 'es' ], 133 | qug: [ 'qu', 'es' ], 134 | rgn: [ 'it' ], 135 | rmy: [ 'ro' ], 136 | 'roa-rup': [ 'rup' ], 137 | rue: [ 'uk', 'ru' ], 138 | ruq: [ 'ruq-latn', 'ro' ], 139 | 'ruq-cyrl': [ 'mk' ], 140 | 'ruq-latn': [ 'ro' ], 141 | sa: [ 'hi' ], 142 | sah: [ 'ru' ], 143 | scn: [ 'it' ], 144 | sg: [ 'fr' ], 145 | sgs: [ 'lt' ], 146 | sli: [ 'de' ], 147 | sr: [ 'sr-ec' ], 148 | srn: [ 'nl' ], 149 | stq: [ 'de' ], 150 | su: [ 'id' ], 151 | szl: [ 'pl' ], 152 | tcy: [ 'kn' ], 153 | tg: [ 'tg-cyrl' ], 154 | tt: [ 'tt-cyrl', 'ru' ], 155 | 'tt-cyrl': [ 'ru' ], 156 | ty: [ 'fr' ], 157 | udm: [ 'ru' ], 158 | ug: [ 'ug-arab' ], 159 | uk: [ 'ru' ], 160 | vec: [ 'it' ], 161 | vep: [ 'et' ], 162 | vls: [ 'nl' ], 163 | vmf: [ 'de' ], 164 | vot: [ 'fi' ], 165 | vro: [ 'et' ], 166 | wa: [ 'fr' ], 167 | wo: [ 'fr' ], 168 | wuu: [ 'zh-hans' ], 169 | xal: [ 'ru' ], 170 | xmf: [ 'ka' ], 171 | yi: [ 'he' ], 172 | za: [ 'zh-hans' ], 173 | zea: [ 'nl' ], 174 | zh: [ 'zh-hans' ], 175 | 'zh-classical': [ 'lzh' ], 176 | 'zh-cn': [ 'zh-hans' ], 177 | 'zh-hant': [ 'zh-hans' ], 178 | 'zh-hk': [ 'zh-hant', 'zh-hans' ], 179 | 'zh-min-nan': [ 'nan' ], 180 | 'zh-mo': [ 'zh-hk', 'zh-hant', 'zh-hans' ], 181 | 'zh-my': [ 'zh-sg', 'zh-hans' ], 182 | 'zh-sg': [ 'zh-hans' ], 183 | 'zh-tw': [ 'zh-hant', 'zh-hans' ], 184 | 'zh-yue': [ 'yue' ] 185 | } ); 186 | }( jQuery ) ); 187 | -------------------------------------------------------------------------------- /plugins/web/public/js/jquery.i18n.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Internationalization library 3 | * 4 | * Copyright (C) 2012 Santhosh Thottingal 5 | * 6 | * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do 7 | * anything special to choose one license or the other and you don't have to 8 | * notify anyone which license you are using. You are free to use 9 | * UniversalLanguageSelector in commercial projects as long as the copyright 10 | * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. 11 | * 12 | * @licence GNU General Public Licence 2.0 or later 13 | * @licence MIT License 14 | */ 15 | 16 | ( function ( $ ) { 17 | 'use strict'; 18 | 19 | var I18N, 20 | slice = Array.prototype.slice; 21 | /** 22 | * @constructor 23 | * @param {Object} options 24 | */ 25 | I18N = function ( options ) { 26 | // Load defaults 27 | this.options = $.extend( {}, I18N.defaults, options ); 28 | 29 | this.parser = this.options.parser; 30 | this.locale = this.options.locale; 31 | this.messageStore = this.options.messageStore; 32 | this.languages = {}; 33 | }; 34 | 35 | I18N.prototype = { 36 | /** 37 | * Localize a given messageKey to a locale. 38 | * @param {String} messageKey 39 | * @return {String} Localized message 40 | */ 41 | localize: function ( messageKey ) { 42 | var localeParts, localePartIndex, locale, fallbackIndex, 43 | tryingLocale, message; 44 | 45 | locale = this.locale; 46 | fallbackIndex = 0; 47 | 48 | while ( locale ) { 49 | // Iterate through locales starting at most-specific until 50 | // localization is found. As in fi-Latn-FI, fi-Latn and fi. 51 | localeParts = locale.split( '-' ); 52 | localePartIndex = localeParts.length; 53 | 54 | do { 55 | tryingLocale = localeParts.slice( 0, localePartIndex ).join( '-' ); 56 | message = this.messageStore.get( tryingLocale, messageKey ); 57 | 58 | if ( message ) { 59 | return message; 60 | } 61 | 62 | localePartIndex--; 63 | } while ( localePartIndex ); 64 | 65 | if ( locale === 'en' ) { 66 | break; 67 | } 68 | 69 | locale = ( $.i18n.fallbacks[ this.locale ] && 70 | $.i18n.fallbacks[ this.locale ][ fallbackIndex ] ) || 71 | this.options.fallbackLocale; 72 | $.i18n.log( 'Trying fallback locale for ' + this.locale + ': ' + locale + ' (' + messageKey + ')' ); 73 | 74 | fallbackIndex++; 75 | } 76 | 77 | // key not found 78 | return ''; 79 | }, 80 | 81 | /* 82 | * Destroy the i18n instance. 83 | */ 84 | destroy: function () { 85 | $.removeData( document, 'i18n' ); 86 | }, 87 | 88 | /** 89 | * General message loading API This can take a URL string for 90 | * the json formatted messages. Example: 91 | * load('path/to/all_localizations.json'); 92 | * 93 | * To load a localization file for a locale: 94 | * 95 | * load('path/to/de-messages.json', 'de' ); 96 | * 97 | * 98 | * To load a localization file from a directory: 99 | * 100 | * load('path/to/i18n/directory', 'de' ); 101 | * 102 | * The above method has the advantage of fallback resolution. 103 | * ie, it will automatically load the fallback locales for de. 104 | * For most usecases, this is the recommended method. 105 | * It is optional to have trailing slash at end. 106 | * 107 | * A data object containing message key- message translation mappings 108 | * can also be passed. Example: 109 | * 110 | * load( { 'hello' : 'Hello' }, optionalLocale ); 111 | * 112 | * 113 | * A source map containing key-value pair of languagename and locations 114 | * can also be passed. Example: 115 | * 116 | * load( { 117 | * bn: 'i18n/bn.json', 118 | * he: 'i18n/he.json', 119 | * en: 'i18n/en.json' 120 | * } ) 121 | * 122 | * 123 | * If the data argument is null/undefined/false, 124 | * all cached messages for the i18n instance will get reset. 125 | * 126 | * @param {string|Object} source 127 | * @param {string} locale Language tag 128 | * @return {jQuery.Promise} 129 | */ 130 | load: function ( source, locale ) { 131 | var fallbackLocales, locIndex, fallbackLocale, sourceMap = {}; 132 | if ( !source && !locale ) { 133 | source = 'i18n/' + $.i18n().locale + '.json'; 134 | locale = $.i18n().locale; 135 | } 136 | if ( typeof source === 'string' && 137 | // source extension should be json, but can have query params after that. 138 | source.split( '?' )[ 0 ].split( '.' ).pop() !== 'json' 139 | ) { 140 | // Load specified locale then check for fallbacks when directory is 141 | // specified in load() 142 | sourceMap[ locale ] = source + '/' + locale + '.json'; 143 | fallbackLocales = ( $.i18n.fallbacks[ locale ] || [] ) 144 | .concat( this.options.fallbackLocale ); 145 | for ( locIndex = 0; locIndex < fallbackLocales.length; locIndex++ ) { 146 | fallbackLocale = fallbackLocales[ locIndex ]; 147 | sourceMap[ fallbackLocale ] = source + '/' + fallbackLocale + '.json'; 148 | } 149 | return this.load( sourceMap ); 150 | } else { 151 | return this.messageStore.load( source, locale ); 152 | } 153 | 154 | }, 155 | 156 | /** 157 | * Does parameter and magic word substitution. 158 | * 159 | * @param {string} key Message key 160 | * @param {Array} parameters Message parameters 161 | * @return {string} 162 | */ 163 | parse: function ( key, parameters ) { 164 | var message = this.localize( key ); 165 | // FIXME: This changes the state of the I18N object, 166 | // should probably not change the 'this.parser' but just 167 | // pass it to the parser. 168 | this.parser.language = $.i18n.languages[ $.i18n().locale ] || $.i18n.languages[ 'default' ]; 169 | if ( message === '' ) { 170 | message = key; 171 | } 172 | return this.parser.parse( message, parameters ); 173 | } 174 | }; 175 | 176 | /** 177 | * Process a message from the $.I18N instance 178 | * for the current document, stored in jQuery.data(document). 179 | * 180 | * @param {string} key Key of the message. 181 | * @param {string} param1 [param...] Variadic list of parameters for {key}. 182 | * @return {string|$.I18N} Parsed message, or if no key was given 183 | * the instance of $.I18N is returned. 184 | */ 185 | $.i18n = function ( key, param1 ) { 186 | var parameters, 187 | i18n = $.data( document, 'i18n' ), 188 | options = typeof key === 'object' && key; 189 | 190 | // If the locale option for this call is different then the setup so far, 191 | // update it automatically. This doesn't just change the context for this 192 | // call but for all future call as well. 193 | // If there is no i18n setup yet, don't do this. It will be taken care of 194 | // by the `new I18N` construction below. 195 | // NOTE: It should only change language for this one call. 196 | // Then cache instances of I18N somewhere. 197 | if ( options && options.locale && i18n && i18n.locale !== options.locale ) { 198 | i18n.locale = options.locale; 199 | } 200 | 201 | if ( !i18n ) { 202 | i18n = new I18N( options ); 203 | $.data( document, 'i18n', i18n ); 204 | } 205 | 206 | if ( typeof key === 'string' ) { 207 | if ( param1 !== undefined ) { 208 | parameters = slice.call( arguments, 1 ); 209 | } else { 210 | parameters = []; 211 | } 212 | 213 | return i18n.parse( key, parameters ); 214 | } else { 215 | // FIXME: remove this feature/bug. 216 | return i18n; 217 | } 218 | }; 219 | 220 | $.fn.i18n = function () { 221 | var i18n = $.data( document, 'i18n' ); 222 | 223 | if ( !i18n ) { 224 | i18n = new I18N(); 225 | $.data( document, 'i18n', i18n ); 226 | } 227 | 228 | return this.each( function () { 229 | var $this = $( this ), 230 | messageKey = $this.data( 'i18n' ), 231 | lBracket, rBracket, type, key; 232 | 233 | if ( messageKey ) { 234 | lBracket = messageKey.indexOf( '[' ); 235 | rBracket = messageKey.indexOf( ']' ); 236 | if ( lBracket !== -1 && rBracket !== -1 && lBracket < rBracket ) { 237 | type = messageKey.slice( lBracket + 1, rBracket ); 238 | key = messageKey.slice( rBracket + 1 ); 239 | if ( type === 'html' ) { 240 | $this.html( i18n.parse( key ) ); 241 | } else { 242 | $this.attr( type, i18n.parse( key ) ); 243 | } 244 | } else { 245 | $this.text( i18n.parse( messageKey ) ); 246 | } 247 | } else { 248 | $this.find( '[data-i18n]' ).i18n(); 249 | } 250 | } ); 251 | }; 252 | 253 | function getDefaultLocale() { 254 | var nav, locale = $( 'html' ).attr( 'lang' ); 255 | 256 | if ( !locale ) { 257 | if ( typeof window.navigator !== undefined ) { 258 | nav = window.navigator; 259 | locale = nav.language || nav.userLanguage || ''; 260 | } else { 261 | locale = ''; 262 | } 263 | } 264 | return locale; 265 | } 266 | 267 | $.i18n.languages = {}; 268 | $.i18n.messageStore = $.i18n.messageStore || {}; 269 | $.i18n.parser = { 270 | // The default parser only handles variable substitution 271 | parse: function ( message, parameters ) { 272 | return message.replace( /\$(\d+)/g, function ( str, match ) { 273 | var index = parseInt( match, 10 ) - 1; 274 | return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match; 275 | } ); 276 | }, 277 | emitter: {} 278 | }; 279 | $.i18n.fallbacks = {}; 280 | $.i18n.debug = false; 281 | $.i18n.log = function ( /* arguments */ ) { 282 | if ( window.console && $.i18n.debug ) { 283 | window.console.log.apply( window.console, arguments ); 284 | } 285 | }; 286 | /* Static members */ 287 | I18N.defaults = { 288 | locale: getDefaultLocale(), 289 | fallbackLocale: 'en', 290 | parser: $.i18n.parser, 291 | messageStore: $.i18n.messageStore 292 | }; 293 | 294 | // Expose constructor 295 | $.i18n.constructor = I18N; 296 | }( jQuery ) ); 297 | -------------------------------------------------------------------------------- /plugins/web/public/js/jquery.i18n.messagestore.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Internationalization library - Message Store 3 | * 4 | * Copyright (C) 2012 Santhosh Thottingal 5 | * 6 | * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do anything special to 7 | * choose one license or the other and you don't have to notify anyone which license you are using. 8 | * You are free to use UniversalLanguageSelector in commercial projects as long as the copyright 9 | * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. 10 | * 11 | * @licence GNU General Public Licence 2.0 or later 12 | * @licence MIT License 13 | */ 14 | 15 | ( function ( $ ) { 16 | 'use strict'; 17 | 18 | var MessageStore = function () { 19 | this.messages = {}; 20 | this.sources = {}; 21 | }; 22 | 23 | function jsonMessageLoader( url ) { 24 | var deferred = $.Deferred(); 25 | 26 | $.getJSON( url ) 27 | .done( deferred.resolve ) 28 | .fail( function ( jqxhr, settings, exception ) { 29 | $.i18n.log( 'Error in loading messages from ' + url + ' Exception: ' + exception ); 30 | // Ignore 404 exception, because we are handling fallabacks explicitly 31 | deferred.resolve(); 32 | } ); 33 | 34 | return deferred.promise(); 35 | } 36 | 37 | /** 38 | * See https://github.com/wikimedia/jquery.i18n/wiki/Specification#wiki-Message_File_Loading 39 | */ 40 | MessageStore.prototype = { 41 | 42 | /** 43 | * General message loading API This can take a URL string for 44 | * the json formatted messages. 45 | * load('path/to/all_localizations.json'); 46 | * 47 | * This can also load a localization file for a locale 48 | * load( 'path/to/de-messages.json', 'de' ); 49 | * 50 | * A data object containing message key- message translation mappings 51 | * can also be passed Eg: 52 | * 53 | * load( { 'hello' : 'Hello' }, optionalLocale ); 54 | * If the data argument is 55 | * null/undefined/false, 56 | * all cached messages for the i18n instance will get reset. 57 | * 58 | * @param {string|Object} source 59 | * @param {string} locale Language tag 60 | * @return {jQuery.Promise} 61 | */ 62 | load: function ( source, locale ) { 63 | var key = null, 64 | deferred = null, 65 | deferreds = [], 66 | messageStore = this; 67 | 68 | if ( typeof source === 'string' ) { 69 | // This is a URL to the messages file. 70 | $.i18n.log( 'Loading messages from: ' + source ); 71 | deferred = jsonMessageLoader( source ) 72 | .done( function ( localization ) { 73 | messageStore.set( locale, localization ); 74 | } ); 75 | 76 | return deferred.promise(); 77 | } 78 | 79 | if ( locale ) { 80 | // source is an key-value pair of messages for given locale 81 | messageStore.set( locale, source ); 82 | 83 | return $.Deferred().resolve(); 84 | } else { 85 | // source is a key-value pair of locales and their source 86 | for ( key in source ) { 87 | if ( Object.prototype.hasOwnProperty.call( source, key ) ) { 88 | locale = key; 89 | // No {locale} given, assume data is a group of languages, 90 | // call this function again for each language. 91 | deferreds.push( messageStore.load( source[ key ], locale ) ); 92 | } 93 | } 94 | return $.when.apply( $, deferreds ); 95 | } 96 | 97 | }, 98 | 99 | /** 100 | * Set messages to the given locale. 101 | * If locale exists, add messages to the locale. 102 | * 103 | * @param {string} locale 104 | * @param {Object} messages 105 | */ 106 | set: function ( locale, messages ) { 107 | if ( !this.messages[ locale ] ) { 108 | this.messages[ locale ] = messages; 109 | } else { 110 | this.messages[ locale ] = $.extend( this.messages[ locale ], messages ); 111 | } 112 | }, 113 | 114 | /** 115 | * 116 | * @param {string} locale 117 | * @param {string} messageKey 118 | * @return {boolean} 119 | */ 120 | get: function ( locale, messageKey ) { 121 | return this.messages[ locale ] && this.messages[ locale ][ messageKey ]; 122 | } 123 | }; 124 | 125 | $.extend( $.i18n.messageStore, new MessageStore() ); 126 | }( jQuery ) ); 127 | -------------------------------------------------------------------------------- /plugins/web/public/js/jquery.i18n/1.0.4/jquery.i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Internationalization library 3 | * 4 | * Copyright (C) 2012 Santhosh Thottingal 5 | * 6 | * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do 7 | * anything special to choose one license or the other and you don't have to 8 | * notify anyone which license you are using. You are free to use 9 | * UniversalLanguageSelector in commercial projects as long as the copyright 10 | * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. 11 | * 12 | * @licence GNU General Public Licence 2.0 or later 13 | * @licence MIT License 14 | */ 15 | 16 | ( function ( $ ) { 17 | 'use strict'; 18 | 19 | var nav, I18N, 20 | slice = Array.prototype.slice; 21 | /** 22 | * @constructor 23 | * @param {Object} options 24 | */ 25 | I18N = function ( options ) { 26 | // Load defaults 27 | this.options = $.extend( {}, I18N.defaults, options ); 28 | 29 | this.parser = this.options.parser; 30 | this.locale = this.options.locale; 31 | this.messageStore = this.options.messageStore; 32 | this.languages = {}; 33 | 34 | this.init(); 35 | }; 36 | 37 | I18N.prototype = { 38 | /** 39 | * Initialize by loading locales and setting up 40 | * String.prototype.toLocaleString and String.locale. 41 | */ 42 | init: function () { 43 | var i18n = this; 44 | 45 | // Set locale of String environment 46 | String.locale = i18n.locale; 47 | 48 | // Override String.localeString method 49 | String.prototype.toLocaleString = function () { 50 | var localeParts, localePartIndex, value, locale, fallbackIndex, 51 | tryingLocale, message; 52 | 53 | value = this.valueOf(); 54 | locale = i18n.locale; 55 | fallbackIndex = 0; 56 | 57 | while ( locale ) { 58 | // Iterate through locales starting at most-specific until 59 | // localization is found. As in fi-Latn-FI, fi-Latn and fi. 60 | localeParts = locale.split( '-' ); 61 | localePartIndex = localeParts.length; 62 | 63 | do { 64 | tryingLocale = localeParts.slice( 0, localePartIndex ).join( '-' ); 65 | message = i18n.messageStore.get( tryingLocale, value ); 66 | 67 | if ( message ) { 68 | return message; 69 | } 70 | 71 | localePartIndex--; 72 | } while ( localePartIndex ); 73 | 74 | if ( locale === 'en' ) { 75 | break; 76 | } 77 | 78 | locale = ( $.i18n.fallbacks[i18n.locale] && $.i18n.fallbacks[i18n.locale][fallbackIndex] ) || 79 | i18n.options.fallbackLocale; 80 | $.i18n.log( 'Trying fallback locale for ' + i18n.locale + ': ' + locale ); 81 | 82 | fallbackIndex++; 83 | } 84 | 85 | // key not found 86 | return ''; 87 | }; 88 | }, 89 | 90 | /* 91 | * Destroy the i18n instance. 92 | */ 93 | destroy: function () { 94 | $.removeData( document, 'i18n' ); 95 | }, 96 | 97 | /** 98 | * General message loading API This can take a URL string for 99 | * the json formatted messages. Example: 100 | * load('path/to/all_localizations.json'); 101 | * 102 | * To load a localization file for a locale: 103 | * 104 | * load('path/to/de-messages.json', 'de' ); 105 | * 106 | * 107 | * To load a localization file from a directory: 108 | * 109 | * load('path/to/i18n/directory', 'de' ); 110 | * 111 | * The above method has the advantage of fallback resolution. 112 | * ie, it will automatically load the fallback locales for de. 113 | * For most usecases, this is the recommended method. 114 | * It is optional to have trailing slash at end. 115 | * 116 | * A data object containing message key- message translation mappings 117 | * can also be passed. Example: 118 | * 119 | * load( { 'hello' : 'Hello' }, optionalLocale ); 120 | * 121 | * 122 | * A source map containing key-value pair of languagename and locations 123 | * can also be passed. Example: 124 | * 125 | * load( { 126 | * bn: 'i18n/bn.json', 127 | * he: 'i18n/he.json', 128 | * en: 'i18n/en.json' 129 | * } ) 130 | * 131 | * 132 | * If the data argument is null/undefined/false, 133 | * all cached messages for the i18n instance will get reset. 134 | * 135 | * @param {String|Object} source 136 | * @param {String} locale Language tag 137 | * @returns {jQuery.Promise} 138 | */ 139 | load: function ( source, locale ) { 140 | var fallbackLocales, locIndex, fallbackLocale, sourceMap = {}; 141 | if ( !source && !locale ) { 142 | source = 'i18n/' + $.i18n().locale + '.json'; 143 | locale = $.i18n().locale; 144 | } 145 | if ( typeof source === 'string' && 146 | source.split( '.' ).pop() !== 'json' 147 | ) { 148 | // Load specified locale then check for fallbacks when directory is specified in load() 149 | sourceMap[locale] = source + '/' + locale + '.json'; 150 | fallbackLocales = ( $.i18n.fallbacks[locale] || [] ) 151 | .concat( this.options.fallbackLocale ); 152 | for ( locIndex in fallbackLocales ) { 153 | fallbackLocale = fallbackLocales[locIndex]; 154 | sourceMap[fallbackLocale] = source + '/' + fallbackLocale + '.json'; 155 | } 156 | return this.load( sourceMap ); 157 | } else { 158 | return this.messageStore.load( source, locale ); 159 | } 160 | 161 | }, 162 | 163 | /** 164 | * Does parameter and magic word substitution. 165 | * 166 | * @param {string} key Message key 167 | * @param {Array} parameters Message parameters 168 | * @return {string} 169 | */ 170 | parse: function ( key, parameters ) { 171 | var message = key.toLocaleString(); 172 | // FIXME: This changes the state of the I18N object, 173 | // should probably not change the 'this.parser' but just 174 | // pass it to the parser. 175 | this.parser.language = $.i18n.languages[$.i18n().locale] || $.i18n.languages['default']; 176 | if ( message === '' ) { 177 | message = key; 178 | } 179 | return this.parser.parse( message, parameters ); 180 | } 181 | }; 182 | 183 | /** 184 | * Process a message from the $.I18N instance 185 | * for the current document, stored in jQuery.data(document). 186 | * 187 | * @param {string} key Key of the message. 188 | * @param {string} param1 [param...] Variadic list of parameters for {key}. 189 | * @return {string|$.I18N} Parsed message, or if no key was given 190 | * the instance of $.I18N is returned. 191 | */ 192 | $.i18n = function ( key, param1 ) { 193 | var parameters, 194 | i18n = $.data( document, 'i18n' ), 195 | options = typeof key === 'object' && key; 196 | 197 | // If the locale option for this call is different then the setup so far, 198 | // update it automatically. This doesn't just change the context for this 199 | // call but for all future call as well. 200 | // If there is no i18n setup yet, don't do this. It will be taken care of 201 | // by the `new I18N` construction below. 202 | // NOTE: It should only change language for this one call. 203 | // Then cache instances of I18N somewhere. 204 | if ( options && options.locale && i18n && i18n.locale !== options.locale ) { 205 | String.locale = i18n.locale = options.locale; 206 | } 207 | 208 | if ( !i18n ) { 209 | i18n = new I18N( options ); 210 | $.data( document, 'i18n', i18n ); 211 | } 212 | 213 | if ( typeof key === 'string' ) { 214 | if ( param1 !== undefined ) { 215 | parameters = slice.call( arguments, 1 ); 216 | } else { 217 | parameters = []; 218 | } 219 | 220 | return i18n.parse( key, parameters ); 221 | } else { 222 | // FIXME: remove this feature/bug. 223 | return i18n; 224 | } 225 | }; 226 | 227 | $.fn.i18n = function () { 228 | var i18n = $.data( document, 'i18n' ); 229 | 230 | if ( !i18n ) { 231 | i18n = new I18N(); 232 | $.data( document, 'i18n', i18n ); 233 | } 234 | String.locale = i18n.locale; 235 | return this.each( function () { 236 | var $this = $( this ), 237 | messageKey = $this.data( 'i18n' ); 238 | 239 | if ( messageKey ) { 240 | $this.text( i18n.parse( messageKey ) ); 241 | } else { 242 | $this.find( '[data-i18n]' ).i18n(); 243 | } 244 | } ); 245 | }; 246 | 247 | String.locale = String.locale || $( 'html' ).attr( 'lang' ); 248 | 249 | if ( !String.locale ) { 250 | if ( typeof window.navigator !== undefined ) { 251 | nav = window.navigator; 252 | String.locale = nav.language || nav.userLanguage || ''; 253 | } else { 254 | String.locale = ''; 255 | } 256 | } 257 | 258 | $.i18n.languages = {}; 259 | $.i18n.messageStore = $.i18n.messageStore || {}; 260 | $.i18n.parser = { 261 | // The default parser only handles variable substitution 262 | parse: function ( message, parameters ) { 263 | return message.replace( /\$(\d+)/g, function ( str, match ) { 264 | var index = parseInt( match, 10 ) - 1; 265 | return parameters[index] !== undefined ? parameters[index] : '$' + match; 266 | } ); 267 | }, 268 | emitter: {} 269 | }; 270 | $.i18n.fallbacks = {}; 271 | $.i18n.debug = false; 272 | $.i18n.log = function ( /* arguments */ ) { 273 | if ( window.console && $.i18n.debug ) { 274 | window.console.log.apply( window.console, arguments ); 275 | } 276 | }; 277 | /* Static members */ 278 | I18N.defaults = { 279 | locale: String.locale, 280 | fallbackLocale: 'en', 281 | parser: $.i18n.parser, 282 | messageStore: $.i18n.messageStore 283 | }; 284 | 285 | // Expose constructor 286 | $.i18n.constructor = I18N; 287 | }( jQuery ) ); 288 | -------------------------------------------------------------------------------- /plugins/web/public/js/jquery.i18n/1.0.4/jquery.i18n.messagestore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Internationalization library - Message Store 3 | * 4 | * Copyright (C) 2012 Santhosh Thottingal 5 | * 6 | * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do anything special to 7 | * choose one license or the other and you don't have to notify anyone which license you are using. 8 | * You are free to use UniversalLanguageSelector in commercial projects as long as the copyright 9 | * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. 10 | * 11 | * @licence GNU General Public Licence 2.0 or later 12 | * @licence MIT License 13 | */ 14 | 15 | ( function ( $, window, undefined ) { 16 | 'use strict'; 17 | 18 | var MessageStore = function () { 19 | this.messages = {}; 20 | this.sources = {}; 21 | }; 22 | 23 | /** 24 | * See https://github.com/wikimedia/jquery.i18n/wiki/Specification#wiki-Message_File_Loading 25 | */ 26 | MessageStore.prototype = { 27 | 28 | /** 29 | * General message loading API This can take a URL string for 30 | * the json formatted messages. 31 | * load('path/to/all_localizations.json'); 32 | * 33 | * This can also load a localization file for a locale 34 | * load( 'path/to/de-messages.json', 'de' ); 35 | * 36 | * A data object containing message key- message translation mappings 37 | * can also be passed Eg: 38 | * 39 | * load( { 'hello' : 'Hello' }, optionalLocale ); 40 | * If the data argument is 41 | * null/undefined/false, 42 | * all cached messages for the i18n instance will get reset. 43 | * 44 | * @param {String|Object} source 45 | * @param {String} locale Language tag 46 | * @return {jQuery.Promise} 47 | */ 48 | load: function ( source, locale ) { 49 | var key = null, 50 | deferred = null, 51 | deferreds = [], 52 | messageStore = this; 53 | 54 | if ( typeof source === 'string' ) { 55 | // This is a URL to the messages file. 56 | $.i18n.log( 'Loading messages from: ' + source ); 57 | deferred = jsonMessageLoader( source ) 58 | .done( function ( localization ) { 59 | messageStore.set( locale, localization ); 60 | } ); 61 | 62 | return deferred.promise(); 63 | } 64 | 65 | if ( locale ) { 66 | // source is an key-value pair of messages for given locale 67 | messageStore.set( locale, source ); 68 | 69 | return $.Deferred().resolve(); 70 | } else { 71 | // source is a key-value pair of locales and their source 72 | for ( key in source ) { 73 | if ( Object.prototype.hasOwnProperty.call( source, key ) ) { 74 | locale = key; 75 | // No {locale} given, assume data is a group of languages, 76 | // call this function again for each language. 77 | deferreds.push( messageStore.load( source[key], locale ) ); 78 | } 79 | } 80 | return $.when.apply( $, deferreds ); 81 | } 82 | 83 | }, 84 | 85 | /** 86 | * Set messages to the given locale. 87 | * If locale exists, add messages to the locale. 88 | * @param locale 89 | * @param messages 90 | */ 91 | set: function ( locale, messages ) { 92 | if ( !this.messages[locale] ) { 93 | this.messages[locale] = messages; 94 | } else { 95 | this.messages[locale] = $.extend( this.messages[locale], messages ); 96 | } 97 | }, 98 | 99 | /** 100 | * 101 | * @param locale 102 | * @param messageKey 103 | * @returns {Boolean} 104 | */ 105 | get: function ( locale, messageKey ) { 106 | return this.messages[locale] && this.messages[locale][messageKey]; 107 | } 108 | }; 109 | 110 | function jsonMessageLoader( url ) { 111 | var deferred = $.Deferred(); 112 | 113 | $.getJSON( url ) 114 | .done( deferred.resolve ) 115 | .fail( function ( jqxhr, settings, exception ) { 116 | $.i18n.log( 'Error in loading messages from ' + url + ' Exception: ' + exception ); 117 | // Ignore 404 exception, because we are handling fallabacks explicitly 118 | deferred.resolve(); 119 | } ); 120 | 121 | return deferred.promise(); 122 | } 123 | 124 | $.extend( $.i18n.messageStore, new MessageStore() ); 125 | }( jQuery, window ) ); 126 | -------------------------------------------------------------------------------- /plugins/web/public/scss/common.scss: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | 3 | .container { 4 | padding: 20px; 5 | max-width: 400px; 6 | margin: auto; 7 | } 8 | 9 | .mi_logo { 10 | width: 48px; 11 | height: 48px; 12 | margin: 30px auto; 13 | } 14 | 15 | .dear_user { 16 | font-size: 16px; 17 | font-weight: bold; 18 | padding: 10px 0; 19 | } 20 | 21 | .help_phone { 22 | padding: 10px 0; 23 | } 24 | 25 | .modal { 26 | display: none; 27 | opacity: 0; 28 | position: fixed; 29 | z-index: 999; 30 | left: 0; 31 | top: 0; 32 | height: 100%; 33 | width: 100%; 34 | transition: opacity .5s; 35 | } 36 | 37 | .allow_btn { 38 | margin-top: 60px; 39 | } 40 | 41 | .modal.visible { 42 | display: block; 43 | opacity: 1; 44 | } 45 | 46 | .modal_mask { 47 | position: absolute; 48 | left: 0; 49 | top: 0; 50 | width: 100%; 51 | height: 100%; 52 | opacity: .4; 53 | filter: alpha(opacity=40); 54 | background-color: black; 55 | } 56 | 57 | .modal_box { 58 | text-align: center; 59 | position: absolute; 60 | bottom: -30px; 61 | width: 90%; 62 | left: 50%; 63 | box-shadow: 0 1px 8px rgba(128, 128, 128, 0.3); 64 | border: 1px solid #d1d1d1; 65 | border-radius: 10px; 66 | overflow: hidden; 67 | background-color: white; 68 | transition: width .5s ease, height .5s ease; 69 | -webkit-transform: translate(-50%, -50%); 70 | transform: translate(-50%, -50%); 71 | -webkit-transform: translate(-50%, -50%, 0); 72 | transform: translate(-50%, -50%, 0); 73 | _top: 0; 74 | _left: 0; 75 | _margin-left: 0; 76 | _margin-top: 0; 77 | max-width: 400px; 78 | } 79 | 80 | .modal_body { 81 | margin: 20px; 82 | text-align: left; 83 | } 84 | 85 | .modal_footer { 86 | display: flex; 87 | border-top: 1px solid #f8f8f8; 88 | } 89 | 90 | .modal_btn { 91 | flex: 1; 92 | padding: 15px 0; 93 | } 94 | 95 | .modal_btn_ok { 96 | color: #ff6700; 97 | border-left: 1px solid #f8f8f8; 98 | } 99 | 100 | .btn { 101 | margin-bottom: 20px; 102 | text-align: center; 103 | padding: 12px 0; 104 | border-radius: 6px; 105 | } 106 | 107 | .btn_ok { 108 | background-color: #ff6700; 109 | color: #fff; 110 | } 111 | 112 | .btn_no { 113 | background-color: #fff; 114 | color: #999; 115 | border: 1px solid #d3d3d3; 116 | } 117 | 118 | .state { 119 | margin-top: 30px; 120 | img { 121 | width: 100px; 122 | height: 100px; 123 | margin-top: 40px; 124 | } 125 | dd { 126 | font-size: 30px; 127 | margin: 20px 0; 128 | } 129 | } 130 | 131 | 132 | .device_list { 133 | margin: 20px 0 20px 30px; 134 | list-style-type: disc; 135 | &.gray { 136 | width: 220px; 137 | color: #999; 138 | margin: 40px auto 0 auto; 139 | } 140 | } 141 | 142 | .caution { 143 | margin: 30px 0; 144 | .line { 145 | width: 100%; 146 | margin-bottom: -10px; 147 | border-bottom: 1px solid #f0f0f0; 148 | } 149 | .txt { 150 | color: #999; 151 | background-color: #fff; 152 | padding: 5px 10px; 153 | } 154 | } 155 | 156 | .tips { 157 | margin: 20px 0; 158 | } 159 | 160 | .icon_gter { 161 | width: 10%; 162 | height: 16px; 163 | display: block; 164 | position: relative; 165 | &:after { 166 | content: ""; 167 | width: 12px; 168 | height: 12px; 169 | border-width: 1px; 170 | border-style: solid; 171 | border-color: transparent transparent rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.3); 172 | -webkit-transform: rotate(-135deg); 173 | transform: rotate(-135deg); 174 | position: absolute; 175 | left: 5px; 176 | top: 4px; 177 | } 178 | } 179 | 180 | .icon_lser { 181 | width: 10%; 182 | height: 16px; 183 | display: block; 184 | position: relative; 185 | &:after { 186 | content: ""; 187 | width: 12px; 188 | height: 12px; 189 | border-width: 1px; 190 | border-style: solid; 191 | border-color: transparent transparent rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.3); 192 | -webkit-transform: rotate(45deg); 193 | transform: rotate(45deg); 194 | position: absolute; 195 | left: 5px; 196 | top: 4px; 197 | } 198 | } 199 | 200 | .active_tips { 201 | text-align: center; 202 | } 203 | 204 | .more_tips { 205 | .tips { 206 | height: 60px; 207 | overflow: auto; 208 | } 209 | } 210 | 211 | .more_sec_info { 212 | font-size: 12px; 213 | } 214 | 215 | 216 | .lang-select-list { 217 | height: 20px; 218 | line-height: 20px; 219 | text-align: center; 220 | li { 221 | font-size: 14px; 222 | display: inline-block; 223 | a { 224 | padding: 10px; 225 | color: #999; 226 | &.current { 227 | color: #ef5b00; 228 | } 229 | } 230 | } 231 | } 232 | 233 | .outlook_tips { 234 | color: #f66; 235 | font-size: 12px; 236 | padding-bottom: 20px; 237 | display: none; 238 | } -------------------------------------------------------------------------------- /plugins/web/public/scss/reset.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | a, 7 | a:hover { 8 | text-decoration: none; 9 | color: #09f; 10 | } 11 | 12 | body { 13 | font-size: 14px; 14 | font-family: arial, "Hiragino Sans GB", "Microsoft YaHei", "微軟正黑體", "儷黑 Pro", sans-serif; 15 | color: #000; 16 | background-color: #fff; 17 | } 18 | 19 | .tac { 20 | text-align: center; 21 | } 22 | 23 | .fl { 24 | float: left; 25 | } 26 | 27 | .fr { 28 | float: right 29 | } 30 | 31 | ul { 32 | list-style: none; 33 | } 34 | 35 | .clearfix { 36 | &:before, 37 | &:after { 38 | content: " "; 39 | display: table; 40 | } 41 | &:after { 42 | clear: both; 43 | } 44 | } -------------------------------------------------------------------------------- /plugins/web/templates/activeSync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 邮箱激活系统 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 32 |

亲爱的

33 |

您的邮箱 正在一台新的设备()上登录,详情如下 34 |

35 |
    36 |

    您使用的是outlook客户端,用户名和密码会被保存到第三方微软公司的服务器中,存在安全风险,推荐使用手机自带的邮件客户端

    37 |

    如果这不是您本人操作,您的邮箱密码可能已经泄露,请登录到内网帐号中心修改密码并拒绝该请求

    38 |

    如需协助请联系IT部门

    39 |
    允许
    40 |
    拒绝
    41 | 51 | 61 |
    62 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /plugins/web/templates/deviceState.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 邮箱激活系统 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
    19 |
    state_logo
    20 |
    21 |
    22 |
      23 |
      24 |
      25 |
      温馨提示
      26 |

      已授权设备请登录到内网账号中心中的“手机邮箱”中进行管理 27 |

      28 |
      29 |

      如果该设备不是您的设备,您的密码可能已经泄漏,请登录到内网账号中心修改密码

      30 |

      已拒绝设备请登录到内网账号中心中的“手机邮箱”中进行管理 31 |

      32 |
      33 |
      34 |

      已经激活的设备数超出了10台的限制,请登录到内网账号中心,在“手机邮箱”中删除不再使用的设备 35 |

      36 |
      37 |
      38 |
      39 |

      40 |
      41 |

      更多安全知识,请访问小米安全中心

      42 |
      43 |
      44 |
      45 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /plugins/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "exchange_proxy/logger" 5 | "exchange_proxy/models" 6 | "exchange_proxy/plugins/active_sync" 7 | 8 | "encoding/json" 9 | "html/template" 10 | "net/http" 11 | ) 12 | 13 | type ( 14 | ApiData struct { 15 | Name string 16 | DeviceModel string 17 | DeviceType string 18 | DeviceId string 19 | Imei string 20 | PhoneNumber string 21 | ActiveStatus string 22 | Code string 23 | } 24 | 25 | RespData struct { 26 | Code int 27 | Data string 28 | Message string 29 | } 30 | ) 31 | 32 | func Activation(w http.ResponseWriter, r *http.Request) { 33 | _ = r.ParseForm() 34 | c := r.FormValue("c") 35 | 36 | tSync, _ := template.ParseFiles("plugins/web/templates/activeSync.html") 37 | tState, _ := template.ParseFiles("plugins/web/templates/deviceState.html") 38 | 39 | var ( 40 | deviceName string 41 | deviceMode string 42 | imei string 43 | deviceId string 44 | deviceNum int 45 | 46 | apiDate ApiData 47 | ) 48 | 49 | if c != "" { 50 | // 检查激活码 51 | err, has, user, deviceInfo := active_sync.CheckActiveCode(c) 52 | // 激活码是存在的 53 | if err == nil && has { 54 | // 获取当前用户的设备数 55 | deviceNum = models.GetDeviceNum(user) 56 | deviceId = deviceInfo.DeviceId 57 | 58 | apiDate.Name = user 59 | apiDate.DeviceId = deviceId 60 | apiDate.DeviceModel = deviceInfo.DeviceType 61 | apiDate.DeviceType = deviceInfo.DeviceType 62 | apiDate.Code = c 63 | 64 | // 取出手机端通过WBXML协议获取的数据 65 | wbxmlInfo, err := models.GetDeviceInfoByDeviceId(deviceId) 66 | if err == nil { 67 | deviceName = wbxmlInfo.FriendlyName 68 | deviceName = wbxmlInfo.Model 69 | imei = wbxmlInfo.IMEI 70 | phone := wbxmlInfo.PhoneNumber 71 | 72 | apiDate.DeviceModel = deviceMode 73 | apiDate.Imei = imei 74 | apiDate.DeviceType = deviceName 75 | apiDate.PhoneNumber = phone 76 | } 77 | CodeStatus := deviceInfo.State 78 | deviceStatue := models.GetDeviceState(user, deviceId) 79 | logger.Log.Infof("user: %v, deviceId: %v, CodeStatus: %v, deviceStatue: %v", user, deviceId, CodeStatus, deviceStatue) 80 | switch CodeStatus { 81 | case 0: 82 | // 显示已激活的状态页面 83 | if deviceStatue == 0 { 84 | apiDate.ActiveStatus = "STATE_ACTIVED" 85 | _ = tState.Execute(w, apiDate) 86 | // 显示已拒绝状态的页面 87 | } else if deviceStatue == 3 { 88 | apiDate.ActiveStatus = "STATE_REJECTED" 89 | _ = tState.Execute(w, apiDate) 90 | } 91 | default: 92 | // 激活的设备数已经超过10个 93 | if deviceNum >= 10 { 94 | apiDate.ActiveStatus = "STATE_EXCEED" 95 | _ = tState.Execute(w, apiDate) 96 | } else { 97 | // 显示激活页面 98 | _ = tSync.Execute(w, apiDate) 99 | } 100 | } 101 | } else { 102 | // 激活码不存在 103 | apiDate.ActiveStatus = "STATE_INVALID" 104 | _ = tState.Execute(w, apiDate) 105 | } 106 | } else { 107 | // 激活码为空 108 | apiDate.ActiveStatus = "STATE_INVALID" 109 | _ = tState.Execute(w, apiDate) 110 | } 111 | } 112 | 113 | func ActiveDevice(w http.ResponseWriter, r *http.Request) { 114 | if r.Method == "POST" { 115 | _ = r.ParseForm() 116 | c := r.FormValue("c") 117 | respData := RespData{Code: 500, Data: "", Message: "设备不存在"} 118 | if c != "" { 119 | result, user, device := active_sync.VerifyActiveCode(c) 120 | if result { 121 | _ = models.ActiveDevice(user, device.DeviceId) 122 | _, _ = active_sync.ResetActiveCodeStatus(c, user, device, 0) 123 | respData.Code = 200 124 | respData.Message = "已经允许设备访问" 125 | } 126 | } 127 | data, _ := json.Marshal(respData) 128 | _, _ = w.Write(data) 129 | } 130 | } 131 | 132 | func IgnoreDevice(w http.ResponseWriter, r *http.Request) { 133 | if r.Method == "POST" { 134 | _ = r.ParseForm() 135 | c := r.FormValue("c") 136 | respData := RespData{Code: 500, Data: "", Message: "设备不存在"} 137 | if c != "" { 138 | result, user, device := active_sync.VerifyActiveCode(c) 139 | if result { 140 | _ = models.IgnoreDevice(user, device.DeviceId) 141 | _, _ = active_sync.ResetActiveCodeStatus(c, user, device, 3) 142 | respData.Code = 200 143 | respData.Message = "已忽略该设备" 144 | } 145 | } 146 | data, _ := json.Marshal(respData) 147 | _, _ = w.Write(data) 148 | } 149 | } 150 | 151 | func NotFound(w http.ResponseWriter, r *http.Request) { 152 | w.WriteHeader(404) 153 | } 154 | -------------------------------------------------------------------------------- /settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "exchange_proxy/logger" 5 | "gopkg.in/ini.v1" 6 | ) 7 | 8 | var ( 9 | Cfg *ini.File 10 | ) 11 | 12 | func init() { 13 | var err error 14 | source := "conf/app.ini" 15 | Cfg, err = ini.Load(source) 16 | 17 | if err != nil { 18 | logger.Log.Panicln(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /util/http_client.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type TokenResult struct { 11 | Code int `json:"code"` 12 | Data string `json:"data"` 13 | } 14 | 15 | func CheckToken(urlOtp, username, token string) (result bool, err error) { 16 | users := make([]string, 0) 17 | users = append(users, username) 18 | tokens := make([]string, 0) 19 | tokens = append(tokens, token) 20 | resp, err := http.PostForm(urlOtp, url.Values{"username": users, "verificationCode": tokens}) 21 | if err == nil { 22 | defer resp.Body.Close() 23 | ret, err := ioutil.ReadAll(resp.Body) 24 | 25 | if err == nil { 26 | tokenResult := TokenResult{} 27 | err = json.Unmarshal(ret, &tokenResult) 28 | if err == nil { 29 | if tokenResult.Code == 200 && tokenResult.Data == "success" { 30 | result = true 31 | } 32 | } 33 | } 34 | } 35 | return result, err 36 | } 37 | -------------------------------------------------------------------------------- /util/lib.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "time" 9 | ) 10 | 11 | // md5 12 | func MD5(s string) (m string) { 13 | h := md5.New() 14 | _, _ = io.WriteString(h, s) 15 | return fmt.Sprintf("%x", h.Sum(nil)) 16 | } 17 | 18 | // 生成固定的随机字符串 19 | func RandomString(n int) string { 20 | var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 21 | 22 | b := make([]rune, n) 23 | for i := range b { 24 | b[i] = letter[rand.Intn(len(letter))] 25 | } 26 | return string(b) 27 | } 28 | 29 | // 生成激活码 30 | func GenerateAtiveCode(username, deviceId string) (code string) { 31 | randString := RandomString(10) 32 | t := fmt.Sprintf("%v%v%v%v", username, deviceId, randString, time.Now().UnixNano()) 33 | code = MD5(t)[5:17] 34 | return code 35 | 36 | } 37 | -------------------------------------------------------------------------------- /util/wbxml/wbxml.go: -------------------------------------------------------------------------------- 1 | package wbxml 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "regexp" 7 | 8 | "exchange_proxy/logger" 9 | 10 | . "github.com/magicmonty/activesync-go/activesync" 11 | . "github.com/magicmonty/activesync-go/activesync/base" 12 | . "github.com/magicmonty/wbxml-go/wbxml" 13 | ) 14 | 15 | /* 16 | ``` 17 | 18 | 19 | 20 | 21 | MIX 2 22 | 888833336669999 23 | MIX 2 24 | Android 8.0.0 25 | +8618599999999 26 | Android/8.0.0-EAS-1.3 27 | 中国联通 (46001) 28 | 29 | 30 | 31 | 32 | MS-EAS-Provisioning-WBXML 33 | 34 | 35 | 36 | ``` 37 | */ 38 | 39 | type ( 40 | Provision struct { 41 | XMLName xml.Name `xml:"Provision"` 42 | Text string `xml:",chardata"` 43 | O string `xml:"O,attr"` 44 | S string `xml:"S,attr"` 45 | DeviceInformation struct { 46 | Text string `xml:",chardata"` 47 | Set struct { 48 | Text string `xml:",chardata"` 49 | Model string `xml:"Model"` 50 | IMEI string `xml:"IMEI"` 51 | FriendlyName string `xml:"FriendlyName"` 52 | OS string `xml:"OS"` 53 | PhoneNumber string `xml:"PhoneNumber"` 54 | UserAgent string `xml:"UserAgent"` 55 | MobileOperator string `xml:"MobileOperator"` 56 | } `xml:"Set"` 57 | } `xml:"DeviceInformation"` 58 | Policies struct { 59 | Text string `xml:",chardata"` 60 | Policy struct { 61 | Text string `xml:",chardata"` 62 | PolicyType string `xml:"PolicyType"` 63 | } `xml:"Policy"` 64 | } `xml:"Policies"` 65 | } 66 | 67 | DeviceInfo struct { 68 | Model string `json:"model"` 69 | IMEI string `json:"imei"` 70 | FriendlyName string `json:"friendly_name"` 71 | PhoneNumber string `json:"phone_number"` 72 | MobileOperator string `json:"mobile_operator"` 73 | } 74 | ) 75 | 76 | func removeInvalidChars(b []byte) []byte { 77 | re := regexp.MustCompile("[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]") 78 | return re.ReplaceAll(b, []byte{}) 79 | } 80 | 81 | func EncodeXML(xmlString []byte) { 82 | xmlString = removeInvalidChars([]byte(xmlString)) 83 | w := bytes.NewBuffer(make([]byte, 0)) 84 | e := NewEncoder( 85 | MakeCodeBook(PROTOCOL_VERSION_14_1), 86 | string(xmlString), 87 | w) 88 | err := e.Encode() 89 | if err != nil { 90 | logger.Log.Println(err.Error()) 91 | } else { 92 | logger.Log.Println(w) 93 | } 94 | } 95 | 96 | func getDecodeResult(data ...byte) string { 97 | var result string 98 | result, _ = Decode(bytes.NewBuffer(data), MakeCodeBook(PROTOCOL_VERSION_14_1)) 99 | return result 100 | } 101 | 102 | func Parse(data []byte) (deviceInfo DeviceInfo, err error) { 103 | xmldata := getDecodeResult(data...) 104 | 105 | out := Provision{} 106 | err = xml.Unmarshal([]byte(xmldata), &out) 107 | if err == nil { 108 | logger.Log.Debugf("Model: %v", out.DeviceInformation.Set.Model) 109 | logger.Log.Debugf("Imie: %v", out.DeviceInformation.Set.IMEI) 110 | logger.Log.Debugf("FriendlyName: %v", out.DeviceInformation.Set.FriendlyName) 111 | logger.Log.Debugf("PhoneNumber: %v", out.DeviceInformation.Set.PhoneNumber) 112 | logger.Log.Debugf("MobileOperator: %v", out.DeviceInformation.Set.MobileOperator) 113 | 114 | deviceInfo.Model = out.DeviceInformation.Set.Model 115 | deviceInfo.IMEI = out.DeviceInformation.Set.IMEI 116 | deviceInfo.FriendlyName = out.DeviceInformation.Set.FriendlyName 117 | deviceInfo.PhoneNumber = out.DeviceInformation.Set.PhoneNumber 118 | deviceInfo.MobileOperator = out.DeviceInformation.Set.MobileOperator 119 | } 120 | 121 | return deviceInfo, err 122 | } 123 | -------------------------------------------------------------------------------- /vars/vars.go: -------------------------------------------------------------------------------- 1 | package vars 2 | 3 | import ( 4 | "exchange_proxy/logger" 5 | "exchange_proxy/settings" 6 | 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/go-redis/redis" 11 | "github.com/sirupsen/logrus" 12 | "github.com/vulcand/oxy/forward" 13 | ) 14 | 15 | type ( 16 | Config struct { 17 | Host []string 18 | Backend string 19 | Port int 20 | TLS bool 21 | Cert string 22 | Key string 23 | DebugLevel string 24 | } 25 | 26 | RedisConfig struct { 27 | Host string 28 | Port int 29 | Db int 30 | Password string 31 | } 32 | ) 33 | 34 | var ( 35 | FwdOWA *forward.Forwarder 36 | FwdSync *forward.Forwarder 37 | 38 | RedisInstance *redis.Client 39 | 40 | MailConfig Config 41 | RedisConf RedisConfig 42 | 43 | RequestCmds = make(map[string]bool) 44 | ResponseCmds = make(map[string]bool) 45 | CurDir string 46 | ) 47 | 48 | // 代理用到的API接口的信息 49 | var ( 50 | // 用户激活的URL 51 | ActiveUrl string 52 | 53 | //OTP接口 54 | OtpUrl string 55 | 56 | // 短信接口的URL,header头名称与值 57 | SmsApiUrl string 58 | SmsApiHeader string 59 | SmsApiKey string 60 | 61 | // 根据用户名查找手机号的接口的URL 62 | UserPhoneUrl string 63 | ) 64 | 65 | func init() { 66 | 67 | sec := settings.Cfg.Section("mail") 68 | hosts := sec.Key("hosts").MustString("mail.xsec.io") 69 | MailConfig.Host = strings.Split(hosts, ",") 70 | MailConfig.Port = sec.Key("port").MustInt(443) 71 | MailConfig.Backend = sec.Key("backend").MustString("https://8.8.8.8") 72 | MailConfig.TLS = sec.Key("ssl").MustBool(true) 73 | MailConfig.Cert = sec.Key("cert").MustString("certs/ca.crt") 74 | MailConfig.Key = sec.Key("key").MustString("certs/ca.key") 75 | MailConfig.DebugLevel = sec.Key("debug_level").MustString("info") 76 | 77 | secRedis := settings.Cfg.Section("redis") 78 | RedisConf.Host = secRedis.Key("host").MustString("127.0.0.1") 79 | RedisConf.Port = secRedis.Key("port").MustInt(6379) 80 | RedisConf.Db = secRedis.Key("db").MustInt(0) 81 | RedisConf.Password = secRedis.Key("password").MustString("passw0rd") 82 | 83 | FwdOWA, _ = forward.New() 84 | FwdSync, _ = forward.New() 85 | 86 | // 获取当前目录 87 | CurDir, _ = GetCurDir() 88 | // 初始化过滤指令列表 89 | initActiveSyncCmds() 90 | // 初始化代理用的到API接口的值 91 | initApiInfo() 92 | // 初始化日志级别 93 | initDebugLevel() 94 | 95 | logger.Log.Printf("host: %v, port: %v, ssl: %v, path: %v", strings.Join(MailConfig.Host, ","), MailConfig.Port, MailConfig.TLS, CurDir) 96 | 97 | } 98 | 99 | // 手机端未激活前的过滤指令列表 100 | func initActiveSyncCmds() { 101 | // 请求的指令 102 | RequestCmds["SendMail"] = true 103 | RequestCmds["FolderCreate"] = true 104 | RequestCmds["FolderDelete"] = true 105 | RequestCmds["FolderUpdate"] = true 106 | RequestCmds["MeetingResponse"] = true 107 | RequestCmds["ItemOperations"] = true 108 | RequestCmds["SmartForward"] = true 109 | RequestCmds["SmartReply"] = true 110 | RequestCmds["MoveItems"] = true 111 | 112 | // 响应的指令 113 | ResponseCmds["Sync"] = true 114 | ResponseCmds["Search"] = true 115 | ResponseCmds["GetAttachment"] = true 116 | ResponseCmds["GetItemEstimate"] = true 117 | ResponseCmds["MeetingResponse"] = true 118 | } 119 | 120 | // 初始化API接口的值 121 | func initApiInfo() { 122 | sec := settings.Cfg.Section("otp") 123 | OtpUrl = sec.Key("url").MustString("") 124 | 125 | sec = settings.Cfg.Section("sms") 126 | SmsApiUrl = sec.Key("url").MustString("") 127 | SmsApiHeader = sec.Key("header").MustString("") 128 | SmsApiKey = sec.Key("key").MustString("") 129 | 130 | secUserInfo := settings.Cfg.Section("user_info") 131 | UserPhoneUrl = secUserInfo.Key("user_phone").MustString("") 132 | ActiveUrl = secUserInfo.Key("active_url").MustString("") 133 | } 134 | 135 | // 初始化log的级别 136 | func initDebugLevel() { 137 | level := strings.ToLower(MailConfig.DebugLevel) 138 | switch level { 139 | case "info": 140 | logger.Log.Logger.SetLevel(logrus.InfoLevel) 141 | case "debug": 142 | logger.Log.Logger.SetLevel(logrus.DebugLevel) 143 | case "warn": 144 | logger.Log.Logger.SetLevel(logrus.WarnLevel) 145 | case "error": 146 | logger.Log.Logger.SetLevel(logrus.ErrorLevel) 147 | case "fatal": 148 | logger.Log.Logger.SetLevel(logrus.FatalLevel) 149 | default: 150 | logger.Log.Logger.SetLevel(logrus.InfoLevel) 151 | } 152 | } 153 | 154 | func GetCurDir() (path string, err error) { 155 | path, err = filepath.Abs(".") 156 | return path, err 157 | } 158 | --------------------------------------------------------------------------------