mirror of
https://github.com/github/rails.git
synced 2026-01-12 08:08:31 -05:00
Compare commits
896 Commits
3-0-github
...
remove-tma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daecedf14d | ||
|
|
76d83c0d5c | ||
|
|
7335865bd9 | ||
|
|
e43316238d | ||
|
|
c3c6f25ec7 | ||
|
|
331461a65e | ||
|
|
fd05501b4d | ||
|
|
0fa76e01de | ||
|
|
1c215bab58 | ||
|
|
c7238a0746 | ||
|
|
71123b2913 | ||
|
|
2eede7e5ac | ||
|
|
507b8182cf | ||
|
|
3df96518be | ||
|
|
84420c7f12 | ||
|
|
c57e85fd13 | ||
|
|
2eca011798 | ||
|
|
f6cf01337f | ||
|
|
0ad86343c6 | ||
|
|
42524c2bf1 | ||
|
|
46f1ddbff9 | ||
|
|
b18f5c9af1 | ||
|
|
18e9b2ffc9 | ||
|
|
9ec3637bc5 | ||
|
|
ba9248e6e3 | ||
|
|
a27559cddf | ||
|
|
e786726603 | ||
|
|
a1d2a22047 | ||
|
|
d43ecd5b32 | ||
|
|
61359bf6ad | ||
|
|
a2beda1177 | ||
|
|
52c895d565 | ||
|
|
74f90612ec | ||
|
|
a6eb61b7e4 | ||
|
|
fe11782158 | ||
|
|
899e99a025 | ||
|
|
e0774e4730 | ||
|
|
60f783d9ce | ||
|
|
6b46d65597 | ||
|
|
fb1588c5ff | ||
|
|
dea5a10f71 | ||
|
|
11dafeaa75 | ||
|
|
bb99aa1149 | ||
|
|
b132992978 | ||
|
|
78a1fda7c8 | ||
|
|
8d02083f23 | ||
|
|
b1c36b7088 | ||
|
|
b2d4142fb7 | ||
|
|
1aae5e70ef | ||
|
|
a2a34133d8 | ||
|
|
79aa54d0c7 | ||
|
|
3ad5fd1879 | ||
|
|
4c3725723f | ||
|
|
c20a4d18e3 | ||
|
|
01a9fbbcca | ||
|
|
8d4ca9edc6 | ||
|
|
d793a56121 | ||
|
|
f424efe97f | ||
|
|
9f7ff621bd | ||
|
|
b0be721dd9 | ||
|
|
8ca8ac379d | ||
|
|
589ce09564 | ||
|
|
6c42c142e2 | ||
|
|
abc06a2f76 | ||
|
|
b0c3d451a2 | ||
|
|
7e86f9b4d2 | ||
|
|
abe97736b8 | ||
|
|
7e0f60d2ed | ||
|
|
3afa5385c9 | ||
|
|
c545331f9e | ||
|
|
cd0ecff00b | ||
|
|
a0c761dc6b | ||
|
|
b5cf2b4b82 | ||
|
|
8378a44ff9 | ||
|
|
4f0c8ef9f1 | ||
|
|
bc302f2aec | ||
|
|
08d94d3f7e | ||
|
|
10ec012f58 | ||
|
|
92fd824480 | ||
|
|
6d916329b8 | ||
|
|
84465a2cc1 | ||
|
|
0fee359278 | ||
|
|
e0eb8e9c65 | ||
|
|
2826324e56 | ||
|
|
1681ede605 | ||
|
|
44db47c63e | ||
|
|
25139ac92c | ||
|
|
0e52a609fd | ||
|
|
df78de2bc8 | ||
|
|
36b91e34f4 | ||
|
|
bdfddb09d7 | ||
|
|
fdfc8e3b9c | ||
|
|
f5ed5c317e | ||
|
|
96183e0f28 | ||
|
|
f2e32e4fd7 | ||
|
|
8beb84fa33 | ||
|
|
a448e74661 | ||
|
|
fb526a0470 | ||
|
|
96c19ff7cc | ||
|
|
9b78af95be | ||
|
|
5a63df211d | ||
|
|
1851596db5 | ||
|
|
0665182950 | ||
|
|
515917f5d8 | ||
|
|
bc52d81306 | ||
|
|
dbbf2fd19c | ||
|
|
9476d628a3 | ||
|
|
7240e8af6a | ||
|
|
f2990620d7 | ||
|
|
17f2fb44c0 | ||
|
|
8c049c6b20 | ||
|
|
761c9cd5db | ||
|
|
a159fd0b8c | ||
|
|
e8b84ab1b4 | ||
|
|
383ea02e38 | ||
|
|
597fb1da94 | ||
|
|
c6e33d30c1 | ||
|
|
a61a39ecd4 | ||
|
|
b64d1fe637 | ||
|
|
6f17422ca7 | ||
|
|
bac12fa5fc | ||
|
|
56fdfeb265 | ||
|
|
881712cf50 | ||
|
|
b2c91983dc | ||
|
|
bdace5d6aa | ||
|
|
0fcb4302e1 | ||
|
|
11361a9e79 | ||
|
|
add3ccbca6 | ||
|
|
d35a67bba3 | ||
|
|
7e79889d1c | ||
|
|
43e2bbe28e | ||
|
|
b154b97ea4 | ||
|
|
15cafbe267 | ||
|
|
12bbc34aca | ||
|
|
8141f0894e | ||
|
|
27651c1fad | ||
|
|
a9ef2fd56c | ||
|
|
ae63d5c90d | ||
|
|
6f3896751a | ||
|
|
5b0f839054 | ||
|
|
a5d8c95a7c | ||
|
|
dec2c4f4e3 | ||
|
|
99cdea7cbe | ||
|
|
c2d13a9a53 | ||
|
|
fb615cd7fd | ||
|
|
4ae4828953 | ||
|
|
f57ca87729 | ||
|
|
7b6383f263 | ||
|
|
257a29d3cc | ||
|
|
8298bef72e | ||
|
|
046c900df2 | ||
|
|
504f7cfbb3 | ||
|
|
0963774c0a | ||
|
|
2d3bc99b0d | ||
|
|
ba9c469113 | ||
|
|
bfbdeeae30 | ||
|
|
67e18c523c | ||
|
|
526f1e5f15 | ||
|
|
f8f4872fcc | ||
|
|
fad166c152 | ||
|
|
78e4d88c70 | ||
|
|
ac42e6951f | ||
|
|
d0d10f51d7 | ||
|
|
69c4e4ce65 | ||
|
|
80473e035a | ||
|
|
70af7efa16 | ||
|
|
56b35afbdd | ||
|
|
0e9190c902 | ||
|
|
449cf50d85 | ||
|
|
05defcd63a | ||
|
|
f8f365346e | ||
|
|
4a745ca670 | ||
|
|
68bfd8a392 | ||
|
|
549b2ad77c | ||
|
|
09a23d2290 | ||
|
|
7d2173ec5c | ||
|
|
cc53229378 | ||
|
|
844da12ba6 | ||
|
|
a9c69f3bb0 | ||
|
|
687d7f52c4 | ||
|
|
cbf36cf57c | ||
|
|
52c922fad1 | ||
|
|
da93d69bcb | ||
|
|
e703fc101b | ||
|
|
85b6d79d8a | ||
|
|
5ed6a8447b | ||
|
|
54a5088cd5 | ||
|
|
08302d2feb | ||
|
|
c7e875abdb | ||
|
|
1ac00a6844 | ||
|
|
e4accdec0c | ||
|
|
b41c3ba154 | ||
|
|
0f44d37d04 | ||
|
|
ed8cabcec2 | ||
|
|
3d6ed50187 | ||
|
|
b760d699a8 | ||
|
|
5796a92433 | ||
|
|
b1a97a4998 | ||
|
|
a815f0c5a3 | ||
|
|
9da7ff8842 | ||
|
|
2ed893bdce | ||
|
|
240f4e944c | ||
|
|
f7e27bd078 | ||
|
|
6a9e188c0c | ||
|
|
aa449141b4 | ||
|
|
a9032c885f | ||
|
|
e8ba5265e0 | ||
|
|
50f3754525 | ||
|
|
4986d5ed04 | ||
|
|
4fef5af9c3 | ||
|
|
e5af56abfe | ||
|
|
9d3bd87045 | ||
|
|
6b0616d1b8 | ||
|
|
d3da1a2c66 | ||
|
|
8e6a044b2b | ||
|
|
50b7c0c104 | ||
|
|
c66013e2c5 | ||
|
|
9e08e196fa | ||
|
|
17b4fd25e4 | ||
|
|
bd9ca9aed0 | ||
|
|
f97da34b4f | ||
|
|
326188c25e | ||
|
|
60e82a3ca2 | ||
|
|
3ff921a65a | ||
|
|
b10bf834b7 | ||
|
|
86f0287993 | ||
|
|
ab2d7c8b5d | ||
|
|
ca5f5d97b9 | ||
|
|
e3f14d12cd | ||
|
|
e53791f8c0 | ||
|
|
48fbe7b0d8 | ||
|
|
55e88eeee4 | ||
|
|
56bb5504dd | ||
|
|
d8f0a58dcb | ||
|
|
a637b5f447 | ||
|
|
25ec61330b | ||
|
|
9d99e610be | ||
|
|
99bcce7ec1 | ||
|
|
a5696e36c6 | ||
|
|
94878c61a3 | ||
|
|
bb2327d9ab | ||
|
|
32b0b5f7b2 | ||
|
|
76608b13d2 | ||
|
|
cd3d30d569 | ||
|
|
74206aeff2 | ||
|
|
5b1f4c51ce | ||
|
|
ec017e158a | ||
|
|
5d979de1a9 | ||
|
|
e535b45c86 | ||
|
|
3570f3e7f6 | ||
|
|
3568c5cee0 | ||
|
|
ddadcc7cf8 | ||
|
|
0706bdce60 | ||
|
|
5d7ad7ba41 | ||
|
|
22b020db3e | ||
|
|
2de364636c | ||
|
|
8e7a64d090 | ||
|
|
6949d6f54f | ||
|
|
c4ef7bb2a0 | ||
|
|
7cbc546d39 | ||
|
|
9ab1154523 | ||
|
|
ad73a3aec4 | ||
|
|
8e679f1854 | ||
|
|
3c1bb40b6b | ||
|
|
77adb4bc20 | ||
|
|
eababa35cf | ||
|
|
555801c908 | ||
|
|
3616141fa2 | ||
|
|
f967b352d2 | ||
|
|
adcfb4e8bd | ||
|
|
6d1344de5e | ||
|
|
f194d65f36 | ||
|
|
aeff1719ab | ||
|
|
6dbc75fd76 | ||
|
|
04fa5af6a6 | ||
|
|
0d767fd24f | ||
|
|
dcf0f97514 | ||
|
|
ce5af2fefe | ||
|
|
aa401bd75a | ||
|
|
9e262de3d8 | ||
|
|
8716ee44e5 | ||
|
|
ec7716abcd | ||
|
|
f6e71c674c | ||
|
|
fb545f4c60 | ||
|
|
4082001331 | ||
|
|
70034d820f | ||
|
|
81e06075b7 | ||
|
|
457a54653d | ||
|
|
5c0ad82236 | ||
|
|
c9e15709ae | ||
|
|
cec44f5838 | ||
|
|
c401102a27 | ||
|
|
d891754535 | ||
|
|
49943a7120 | ||
|
|
2401af4d6f | ||
|
|
fd5c6e2c97 | ||
|
|
8b79c7c202 | ||
|
|
4b36dafb35 | ||
|
|
8be3e09fcf | ||
|
|
ef0591efc2 | ||
|
|
c55cdd816c | ||
|
|
c519215aa8 | ||
|
|
18ba648e0d | ||
|
|
b8b568e53b | ||
|
|
5efb1503dd | ||
|
|
c69dc1afaa | ||
|
|
a84e9b4f31 | ||
|
|
dae247316d | ||
|
|
2cd29f4297 | ||
|
|
d91d6fe15f | ||
|
|
efec9b24db | ||
|
|
56c5290fce | ||
|
|
4a02437a8d | ||
|
|
958b0e977a | ||
|
|
f87a518f81 | ||
|
|
fcec7402eb | ||
|
|
0653a6d30e | ||
|
|
cfb31edb54 | ||
|
|
1d7368200f | ||
|
|
642d5d297e | ||
|
|
e617af13a2 | ||
|
|
a72bcdb8ae | ||
|
|
19161e08b3 | ||
|
|
e2ec41a9a6 | ||
|
|
2c148cd96a | ||
|
|
de7925de8a | ||
|
|
aa48c79ae4 | ||
|
|
1668ad3baf | ||
|
|
1f44fc90c6 | ||
|
|
0e57c70baf | ||
|
|
a0454dcd1a | ||
|
|
e329eab0c9 | ||
|
|
4b08679ba9 | ||
|
|
ccb1beeb5b | ||
|
|
ea6ef768a7 | ||
|
|
bf563bd904 | ||
|
|
715b34fdff | ||
|
|
00b95eb265 | ||
|
|
03d5d0b5f5 | ||
|
|
bc2af911f9 | ||
|
|
0f18904e2b | ||
|
|
cf7ed7cf2d | ||
|
|
c1b2200085 | ||
|
|
157c1808b9 | ||
|
|
ac7b5a23ba | ||
|
|
0022fa309b | ||
|
|
97e07a88fe | ||
|
|
629afe9f19 | ||
|
|
26f2cce232 | ||
|
|
c0137f62d4 | ||
|
|
f175d19e2a | ||
|
|
9cfa87519d | ||
|
|
2310aef29b | ||
|
|
27aa22826c | ||
|
|
2d3c58068c | ||
|
|
374e49b467 | ||
|
|
ebf300f41b | ||
|
|
b3d32a5b28 | ||
|
|
b99914cc3e | ||
|
|
9b209e8cb8 | ||
|
|
056f957b22 | ||
|
|
40c393cb84 | ||
|
|
eed8a8863d | ||
|
|
ec760a57d1 | ||
|
|
cbc0201a3e | ||
|
|
abb8fbde73 | ||
|
|
5a806f6759 | ||
|
|
f6f75e84c5 | ||
|
|
d3a8152203 | ||
|
|
3969148a13 | ||
|
|
1e64cdf8c9 | ||
|
|
f56a1631be | ||
|
|
f4ce042795 | ||
|
|
ed7322f336 | ||
|
|
b06e5dce97 | ||
|
|
5695b1bdd9 | ||
|
|
268c9040d5 | ||
|
|
63a7ef0d74 | ||
|
|
3a3fa7f817 | ||
|
|
5b4e7c3fa0 | ||
|
|
0307dbaba9 | ||
|
|
39bcf14b34 | ||
|
|
24911757de | ||
|
|
64d28f61ad | ||
|
|
397262a4ee | ||
|
|
62c802c622 | ||
|
|
d7ee4bbcfa | ||
|
|
3926107aff | ||
|
|
6227ec11f0 | ||
|
|
4158282e32 | ||
|
|
6451e864b9 | ||
|
|
84541c4997 | ||
|
|
6f9f1d3aef | ||
|
|
1bf79f19a0 | ||
|
|
16b6d4216f | ||
|
|
f85ab90e4f | ||
|
|
4645cd1499 | ||
|
|
d53a590594 | ||
|
|
9a042baefb | ||
|
|
696ec1f979 | ||
|
|
564ace6a01 | ||
|
|
8a0e8f0669 | ||
|
|
d5b4f4debf | ||
|
|
83b4c161fc | ||
|
|
55c1a86ea4 | ||
|
|
9ca6df83f6 | ||
|
|
0c0da1a6e0 | ||
|
|
b362b394f6 | ||
|
|
02e51500d5 | ||
|
|
158e7b63ab | ||
|
|
3f7729a66e | ||
|
|
ce50c960c1 | ||
|
|
57337cd74d | ||
|
|
652bdeb3c3 | ||
|
|
ef10988fdd | ||
|
|
6012e575bb | ||
|
|
c50609c9f0 | ||
|
|
94de32b6eb | ||
|
|
4682035381 | ||
|
|
6e9b01fddb | ||
|
|
51e6124e6a | ||
|
|
c9a3929a75 | ||
|
|
eb22c248de | ||
|
|
2aef092625 | ||
|
|
d5ba7c3ea4 | ||
|
|
45e192d05c | ||
|
|
0dbe0f670e | ||
|
|
6d056c7175 | ||
|
|
b3bd101796 | ||
|
|
3d17d79bbf | ||
|
|
4b7a439bd1 | ||
|
|
c48a71c7e4 | ||
|
|
f12dd62d47 | ||
|
|
e20ac99026 | ||
|
|
69db137f89 | ||
|
|
b7fd42626a | ||
|
|
319e4aa263 | ||
|
|
1012dec88f | ||
|
|
f32c96eb76 | ||
|
|
c362a76d00 | ||
|
|
b3dd14adbe | ||
|
|
f5714abc3d | ||
|
|
37c51594b9 | ||
|
|
12f6fd0f26 | ||
|
|
54bc4852ea | ||
|
|
90f001ba39 | ||
|
|
7074c5a629 | ||
|
|
146a750568 | ||
|
|
3470b306bb | ||
|
|
603b28c84c | ||
|
|
cca75ca23f | ||
|
|
592085be83 | ||
|
|
ed5b89483a | ||
|
|
ab09ffd41e | ||
|
|
894c9b112c | ||
|
|
1684aa113e | ||
|
|
2675e4ef83 | ||
|
|
a900205676 | ||
|
|
5374fb3cad | ||
|
|
8bb3b9bcff | ||
|
|
e6cadd422b | ||
|
|
48cd7dfcf8 | ||
|
|
1c3711b21b | ||
|
|
b1bbf90dff | ||
|
|
1229ef7bf3 | ||
|
|
6d1494cec6 | ||
|
|
0200e20f14 | ||
|
|
e4ebaab1cb | ||
|
|
754bbaaf37 | ||
|
|
f039bbb13e | ||
|
|
fda3ac6e68 | ||
|
|
1d9468b44e | ||
|
|
b9f352316e | ||
|
|
64082b350c | ||
|
|
b92ff78df6 | ||
|
|
43d2cb8e93 | ||
|
|
ccb197b2e6 | ||
|
|
c253d7b2db | ||
|
|
2f1ded3067 | ||
|
|
1db3a27961 | ||
|
|
da61a6c967 | ||
|
|
bfe0328580 | ||
|
|
3719bd3e95 | ||
|
|
c7057a213d | ||
|
|
ec542caf58 | ||
|
|
27b935dec6 | ||
|
|
d796fc638f | ||
|
|
0f0f977625 | ||
|
|
0bc150c884 | ||
|
|
93085a7d6c | ||
|
|
a942d66597 | ||
|
|
df0720b8b7 | ||
|
|
ed320cd896 | ||
|
|
88d2e4ca6f | ||
|
|
6da0365383 | ||
|
|
78f5ecf02f | ||
|
|
6950f0cef2 | ||
|
|
4c3477ce77 | ||
|
|
4ffe2e171a | ||
|
|
90f3272f1d | ||
|
|
c27cb5e1d4 | ||
|
|
9abab5ba02 | ||
|
|
61bb491cbe | ||
|
|
3fa8ca5845 | ||
|
|
2d67ef9416 | ||
|
|
f62a8831fe | ||
|
|
3a3dfe4996 | ||
|
|
de0384008c | ||
|
|
4bc58a215f | ||
|
|
a4540128a4 | ||
|
|
6c0028d5cc | ||
|
|
22e1f4b307 | ||
|
|
b45d44cff0 | ||
|
|
40f0ef7a37 | ||
|
|
ea345a540f | ||
|
|
ae5e2f5919 | ||
|
|
8073e47262 | ||
|
|
a0d28dcfd4 | ||
|
|
17fda24523 | ||
|
|
a3ab2b0ee1 | ||
|
|
bfb931b865 | ||
|
|
97a7cdca17 | ||
|
|
b68861a00f | ||
|
|
2ccd4e790e | ||
|
|
8820bb7eff | ||
|
|
fd0289f3dd | ||
|
|
a3e129f79b | ||
|
|
d81c606fba | ||
|
|
9ea850027c | ||
|
|
d988507dca | ||
|
|
6fdd60e65c | ||
|
|
1a93e93d1b | ||
|
|
d36f8a2bf2 | ||
|
|
df9a47e4b8 | ||
|
|
6b2291f330 | ||
|
|
c3ef028b81 | ||
|
|
4ae03b2d5a | ||
|
|
7ba80252a5 | ||
|
|
f5f7c40f3a | ||
|
|
fdf356d74b | ||
|
|
9edfdef2a7 | ||
|
|
eb30c69544 | ||
|
|
459749c30c | ||
|
|
e10b0ddc7b | ||
|
|
bbaf3a04f5 | ||
|
|
7454d1874c | ||
|
|
c23706b787 | ||
|
|
4010b49de8 | ||
|
|
c47c5af1c8 | ||
|
|
48b30608a4 | ||
|
|
95b7e4f7d7 | ||
|
|
80da8eb43d | ||
|
|
a69316b293 | ||
|
|
e2127991a1 | ||
|
|
6f2c4991ef | ||
|
|
0f14d7b6d3 | ||
|
|
58f14438a9 | ||
|
|
91b61a8d16 | ||
|
|
f98302e46b | ||
|
|
11cce5bde9 | ||
|
|
1d7412b135 | ||
|
|
1901747001 | ||
|
|
f95a7f896e | ||
|
|
26f22a28e9 | ||
|
|
f489b3341c | ||
|
|
b372b4c875 | ||
|
|
14a6794a8e | ||
|
|
8371d6f0c1 | ||
|
|
4a11ca1c7e | ||
|
|
b18248ff05 | ||
|
|
deac481eb7 | ||
|
|
cddd4746f9 | ||
|
|
c680f2372e | ||
|
|
6222ac1a91 | ||
|
|
e18752868a | ||
|
|
1488c6cc9e | ||
|
|
4f5cac53b7 | ||
|
|
179b4512d1 | ||
|
|
ff0377dea5 | ||
|
|
f503a483d4 | ||
|
|
abd7bd311a | ||
|
|
09b197f957 | ||
|
|
c6fe042b29 | ||
|
|
8dca666ba1 | ||
|
|
477dfa4c79 | ||
|
|
8afeec20e0 | ||
|
|
6ddb7de407 | ||
|
|
2524ac84e6 | ||
|
|
c0245493cb | ||
|
|
9290051b85 | ||
|
|
c665faac09 | ||
|
|
a070873771 | ||
|
|
11c338735c | ||
|
|
c52a50ec51 | ||
|
|
2420d6272c | ||
|
|
55bc0c76f8 | ||
|
|
7c1e4ef64b | ||
|
|
52a50db6c0 | ||
|
|
d48d3d0f41 | ||
|
|
3f59a73cb6 | ||
|
|
08d15f86c4 | ||
|
|
c5e3309bb4 | ||
|
|
44fbc86ab8 | ||
|
|
596406f90a | ||
|
|
3413643e83 | ||
|
|
028d449fe9 | ||
|
|
5de75398c4 | ||
|
|
d2cf33e903 | ||
|
|
ab9efe9e16 | ||
|
|
a32eeebdcb | ||
|
|
e1b109633c | ||
|
|
8a2cfe9de4 | ||
|
|
a43ef2436c | ||
|
|
9a68c72b4b | ||
|
|
07c69380cf | ||
|
|
1f07a89c59 | ||
|
|
2b82708b0e | ||
|
|
594a281d66 | ||
|
|
6bf17770af | ||
|
|
6fdfe4cb5f | ||
|
|
49c4a79e59 | ||
|
|
49342d1745 | ||
|
|
ff8cb50f25 | ||
|
|
70ed47f5b4 | ||
|
|
13fb26b714 | ||
|
|
05d7409ae5 | ||
|
|
058459dc22 | ||
|
|
8a49183563 | ||
|
|
4240890b28 | ||
|
|
e46e67c71f | ||
|
|
3cd245b7fa | ||
|
|
a4838ee466 | ||
|
|
d725ad39da | ||
|
|
e213f0caaa | ||
|
|
38d6e65c5a | ||
|
|
f1355e6a4d | ||
|
|
d6a944f778 | ||
|
|
9127c5b7f5 | ||
|
|
5f6e788e27 | ||
|
|
f3c7bbeedd | ||
|
|
b9f668ea94 | ||
|
|
d8ae3d5a8b | ||
|
|
1cb433ce78 | ||
|
|
14b6ab0f01 | ||
|
|
1cf32ad35a | ||
|
|
75a483e18e | ||
|
|
061b0ba6cb | ||
|
|
dbc62ad225 | ||
|
|
a249cad5ef | ||
|
|
dad0f62dc9 | ||
|
|
6f5d1f3190 | ||
|
|
e82b43599e | ||
|
|
a91969803e | ||
|
|
9284bcc35a | ||
|
|
9a42096e95 | ||
|
|
4e014379a3 | ||
|
|
25fe43bc14 | ||
|
|
9e96f37edd | ||
|
|
c3da22c042 | ||
|
|
9341655fa3 | ||
|
|
5c74cffae6 | ||
|
|
9d51f62866 | ||
|
|
9a3a7983c3 | ||
|
|
e972acc0d7 | ||
|
|
d0bdff0799 | ||
|
|
011baa0f65 | ||
|
|
9bc80f4dd1 | ||
|
|
8a49af3158 | ||
|
|
ff643ce967 | ||
|
|
ebb6606a4d | ||
|
|
15fd67e9d8 | ||
|
|
33c054d7e0 | ||
|
|
cc3183d4be | ||
|
|
9e29c084eb | ||
|
|
ecc9b705d7 | ||
|
|
491f1b5f36 | ||
|
|
b763858ed5 | ||
|
|
9bcacf4962 | ||
|
|
18b4ac6992 | ||
|
|
ba961250bd | ||
|
|
59c3b0d0de | ||
|
|
be017fd7d5 | ||
|
|
93f5d9d5f0 | ||
|
|
7908cfabf7 | ||
|
|
39de15f136 | ||
|
|
250e718355 | ||
|
|
323f58f19f | ||
|
|
29a5549b34 | ||
|
|
c9d4bcf163 | ||
|
|
2c4f4a8734 | ||
|
|
de0b073f3e | ||
|
|
f6f04f1549 | ||
|
|
a8286af3c3 | ||
|
|
d44b5c6219 | ||
|
|
c41fb5865f | ||
|
|
8058a1d7d7 | ||
|
|
d3a802cee0 | ||
|
|
80d8608102 | ||
|
|
d1202cfeb2 | ||
|
|
8056c57a94 | ||
|
|
5e4b946927 | ||
|
|
5f6623b1b4 | ||
|
|
4c96030d05 | ||
|
|
2d2216fadb | ||
|
|
32c23552f5 | ||
|
|
83cc7de2a4 | ||
|
|
ee8fe3ae4e | ||
|
|
3cad1df22e | ||
|
|
7d40ba1cbf | ||
|
|
0b95a2afab | ||
|
|
9aa9bad024 | ||
|
|
a25296ab05 | ||
|
|
ac9f9a9c3e | ||
|
|
796b7c6ce6 | ||
|
|
819c347f43 | ||
|
|
407fbb5090 | ||
|
|
389449d9ae | ||
|
|
d39c45690e | ||
|
|
679a0bf17f | ||
|
|
cd5e784389 | ||
|
|
791c388039 | ||
|
|
ab6d295ce4 | ||
|
|
6843fb9265 | ||
|
|
9aaeb18781 | ||
|
|
1c6c216d91 | ||
|
|
f73d34c131 | ||
|
|
e1d27eedce | ||
|
|
4b33155428 | ||
|
|
8dab61d146 | ||
|
|
55501b9f6a | ||
|
|
5a0e295911 | ||
|
|
95db8aaa5f | ||
|
|
bf00de03de | ||
|
|
6cbcfffeb1 | ||
|
|
f09ceb55c0 | ||
|
|
dc559f274f | ||
|
|
7244425b93 | ||
|
|
21029451d7 | ||
|
|
32cfd4c2f8 | ||
|
|
d37ac7958f | ||
|
|
17f336e2f0 | ||
|
|
523f3ba8da | ||
|
|
60122e81a3 | ||
|
|
ead5d88bf1 | ||
|
|
143c55d325 | ||
|
|
be4d743645 | ||
|
|
7a427a83ca | ||
|
|
c7bcbb983f | ||
|
|
a147becfb8 | ||
|
|
b3ec7b2d03 | ||
|
|
ae85927ea8 | ||
|
|
f36d9a6758 | ||
|
|
18e68d9524 | ||
|
|
6a50d6c8e3 | ||
|
|
bc2c4a4595 | ||
|
|
29c5985849 | ||
|
|
d8f8066cd1 | ||
|
|
31254bedae | ||
|
|
6673001a5e | ||
|
|
944f4fc7d2 | ||
|
|
1c855ad4e7 | ||
|
|
d8fff7d9d5 | ||
|
|
281c1a82de | ||
|
|
f6f24b71a4 | ||
|
|
0d3c5f0a82 | ||
|
|
4d8fd8d335 | ||
|
|
5217c16b09 | ||
|
|
97ad936148 | ||
|
|
dfdf8e5dab | ||
|
|
a8bd3c8a10 | ||
|
|
e10305f0f4 | ||
|
|
eb52dc3db7 | ||
|
|
9407f6e9a4 | ||
|
|
6720b25b2d | ||
|
|
22554745b7 | ||
|
|
8b9b954f40 | ||
|
|
b75bc05bc5 | ||
|
|
a491b19502 | ||
|
|
447d60e9ed | ||
|
|
cb9429a259 | ||
|
|
25fde77674 | ||
|
|
d3d4822262 | ||
|
|
7e1bcef985 | ||
|
|
19c38a9b70 | ||
|
|
f43404d42b | ||
|
|
cd14a4a00e | ||
|
|
898a8801ff | ||
|
|
72d111a21c | ||
|
|
d63fab344f | ||
|
|
c5c022c705 | ||
|
|
d97073337c | ||
|
|
b1a044b629 | ||
|
|
2c3d2906b2 | ||
|
|
056ddbdcfb | ||
|
|
5fb66a3abb | ||
|
|
e70272e2a4 | ||
|
|
63d0c33787 | ||
|
|
f9b2227649 | ||
|
|
756e82d1b6 | ||
|
|
74f16a56e7 | ||
|
|
f1e75e4378 | ||
|
|
cc5d313a48 | ||
|
|
91727ae5e4 | ||
|
|
91fbdfd5b3 | ||
|
|
05abd7c196 | ||
|
|
aebd1ba5b4 | ||
|
|
ec10f13939 | ||
|
|
01f820c3b2 | ||
|
|
a69ee11968 | ||
|
|
4a78dae2ab | ||
|
|
4b4164e8a8 | ||
|
|
84a755b27e | ||
|
|
b600bf2cd7 | ||
|
|
4d7c597e84 | ||
|
|
a92790ab86 | ||
|
|
34c3162c5c | ||
|
|
c73cf7d2c0 | ||
|
|
14edaa104d | ||
|
|
dc94c09503 | ||
|
|
34a1ed0df8 | ||
|
|
4196616778 | ||
|
|
b4c7b3e893 | ||
|
|
9b2a665aff | ||
|
|
a70c78177a | ||
|
|
542d6a0abd | ||
|
|
2a657725f1 | ||
|
|
6339e5d360 | ||
|
|
9fcadcbd68 | ||
|
|
ad85771221 | ||
|
|
50608ecccd | ||
|
|
7a85927da2 | ||
|
|
97b75c9f16 | ||
|
|
dbb0258279 | ||
|
|
2b5e4f38f5 | ||
|
|
5b80ead2a3 | ||
|
|
cc47d3ff0c | ||
|
|
d052e9fb58 | ||
|
|
43e537b9e8 | ||
|
|
e30016c29e | ||
|
|
f383a4aa33 | ||
|
|
14b769899c | ||
|
|
d5f018eb10 | ||
|
|
4cd40726eb | ||
|
|
ba92e83bcc | ||
|
|
66ead4f148 | ||
|
|
f7cb7fce4c | ||
|
|
0380e9ca5f | ||
|
|
35e1785081 | ||
|
|
2bcb2443a9 | ||
|
|
4051dd3412 | ||
|
|
88d5e3341d | ||
|
|
7bf9bf3dd6 | ||
|
|
6dec3c45fc | ||
|
|
e61cceb37f | ||
|
|
7f1f16c01f | ||
|
|
5ac05f15c6 | ||
|
|
49169f7a6a | ||
|
|
ebe8dd6108 | ||
|
|
17e712d3a3 | ||
|
|
93c557828e | ||
|
|
628b4ad679 | ||
|
|
a5ed7eede6 | ||
|
|
d1d1894c2f | ||
|
|
7c4b325e0a | ||
|
|
00eee49e1e | ||
|
|
6e3bede928 | ||
|
|
4b68debb1c | ||
|
|
2633108e1f | ||
|
|
5e57e2fa58 | ||
|
|
cb9a1f17f0 | ||
|
|
61a1456937 | ||
|
|
16f36b6171 | ||
|
|
2d9b45722c | ||
|
|
617d7eb57b | ||
|
|
09a976ac58 | ||
|
|
5bbd097ce9 | ||
|
|
3267097393 | ||
|
|
5a4603fafb | ||
|
|
70ba90b072 | ||
|
|
de4cc53f74 | ||
|
|
6060123470 | ||
|
|
fc2421b784 | ||
|
|
375e8976e3 | ||
|
|
35c5727ace | ||
|
|
878aec9d95 | ||
|
|
c9a3d99164 | ||
|
|
fa750e08a8 | ||
|
|
50e867480a | ||
|
|
dc69d9308a | ||
|
|
ace154d067 | ||
|
|
651611999d | ||
|
|
6a1267a0b1 | ||
|
|
d2e6a0fbc3 | ||
|
|
dace54b2e9 | ||
|
|
daffa5cbdd | ||
|
|
c91912700d |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
pkg
|
||||
.bundle
|
||||
debug.log
|
||||
doc/rdoc
|
||||
activeresource/doc
|
||||
@@ -15,6 +17,3 @@ railties/test/500.html
|
||||
railties/doc/guides/html/images
|
||||
railties/doc/guides/html/stylesheets
|
||||
railties/guides/output
|
||||
*.rbc
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
12
Rakefile
12
Rakefile
@@ -1,10 +1,9 @@
|
||||
require 'rake'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/contrib/sshpublisher'
|
||||
require 'rdoc/task'
|
||||
|
||||
env = %(PKG_BUILD="#{ENV['PKG_BUILD']}") if ENV['PKG_BUILD']
|
||||
|
||||
PROJECTS = %w(activesupport actionpack actionmailer activeresource activerecord railties)
|
||||
PROJECTS = %w(activesupport railties actionpack actionmailer activeresource activerecord)
|
||||
|
||||
Dir["#{File.dirname(__FILE__)}/*/lib/*/version.rb"].each do |version_path|
|
||||
require version_path
|
||||
@@ -13,7 +12,7 @@ end
|
||||
desc 'Run all tests by default'
|
||||
task :default => :test
|
||||
|
||||
%w(test rdoc pgem package release).each do |task_name|
|
||||
%w(test rdoc pgem package release gem).each do |task_name|
|
||||
desc "Run #{task_name} task for all projects"
|
||||
task task_name do
|
||||
PROJECTS.each do |project|
|
||||
@@ -24,13 +23,15 @@ end
|
||||
|
||||
|
||||
desc "Generate documentation for the Rails framework"
|
||||
Rake::RDocTask.new do |rdoc|
|
||||
RDoc::Task.new do |rdoc|
|
||||
rdoc.rdoc_dir = 'doc/rdoc'
|
||||
rdoc.title = "Ruby on Rails Documentation"
|
||||
rdoc.main = "railties/README"
|
||||
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.options << '-A cattr_accessor=object'
|
||||
rdoc.options << '--charset' << 'utf-8'
|
||||
rdoc.options << '--main' << 'railties/README'
|
||||
|
||||
rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : './doc/template/horo'
|
||||
|
||||
@@ -74,6 +75,7 @@ end
|
||||
|
||||
desc "Publish API docs for Rails as a whole and for each component"
|
||||
task :pdoc => :rdoc do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/api", "doc/rdoc").upload
|
||||
PROJECTS.each do |project|
|
||||
system %(cd #{project} && #{env} #{$0} pdoc)
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
*2.3.11 (February 9, 2011)*
|
||||
*2.3.10 (October 15, 2010)*
|
||||
*2.3.9 (September 4, 2010)*
|
||||
*2.3.8 (May 24, 2010)*
|
||||
*2.3.7 (May 24, 2010)*
|
||||
|
||||
* Version bump.
|
||||
|
||||
|
||||
*2.3.6 (May 23, 2010)*
|
||||
|
||||
* Upgrade TMail from 1.2.3 to 1.2.7. [Mikel Lindsaar]
|
||||
|
||||
|
||||
*2.3.5 (November 25, 2009)*
|
||||
|
||||
* Minor Bug Fixes and deprecation warnings
|
||||
|
||||
|
||||
*2.3.4 (September 4, 2009)*
|
||||
|
||||
* Minor bug fixes.
|
||||
|
||||
|
||||
*2.3.3 (July 12, 2009)*
|
||||
|
||||
* No changes, just a version bump.
|
||||
|
||||
|
||||
*2.3.2 [Final] (March 15, 2009)*
|
||||
|
||||
* Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rdoc/task'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/sshpublisher'
|
||||
require 'rubygems/package_task'
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
@@ -30,7 +29,7 @@ Rake::TestTask.new { |t|
|
||||
|
||||
|
||||
# Generate the RDoc documentation
|
||||
Rake::RDocTask.new { |rdoc|
|
||||
RDoc::Task.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Action Mailer -- Easy email delivery and testing"
|
||||
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
||||
@@ -55,19 +54,17 @@ spec = Gem::Specification.new do |s|
|
||||
s.rubyforge_project = "actionmailer"
|
||||
s.homepage = "http://www.rubyonrails.org"
|
||||
|
||||
s.add_dependency('actionpack', '= 2.3.2' + PKG_BUILD)
|
||||
s.add_dependency('actionpack', '= 2.3.14' + PKG_BUILD)
|
||||
|
||||
s.has_rdoc = true
|
||||
s.requirements << 'none'
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'action_mailer'
|
||||
|
||||
s.files = [ "Rakefile", "install.rb", "README", "CHANGELOG", "MIT-LICENSE" ]
|
||||
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
Gem::PackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
@@ -76,12 +73,14 @@ end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pgem => [:package] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#--
|
||||
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
# Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -58,5 +58,3 @@ module Net
|
||||
end
|
||||
|
||||
autoload :MailHelper, 'action_mailer/mail_helper'
|
||||
|
||||
require 'action_mailer/vendor/tmail'
|
||||
|
||||
@@ -195,6 +195,39 @@ module ActionMailer #:nodoc:
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# = Multipart Emails with Attachments
|
||||
#
|
||||
# Multipart emails that also have attachments can be created by nesting a "multipart/alternative" part
|
||||
# within an email that has its content type set to "multipart/mixed". This would also need two templates
|
||||
# in place within +app/views/mailer+ called "welcome_email.text.html.erb" and "welcome_email.text.plain.erb"
|
||||
#
|
||||
# class ApplicationMailer < ActionMailer::Base
|
||||
# def signup_notification(recipient)
|
||||
# recipients recipient.email_address_with_name
|
||||
# subject "New account information"
|
||||
# from "system@example.com"
|
||||
# content_type "multipart/mixed"
|
||||
#
|
||||
# part "multipart/alternative" do |alternative|
|
||||
#
|
||||
# alternative.part "text/html" do |html|
|
||||
# html.body = render_message("welcome_email.text.html", :message => "<h1>HTML content</h1>")
|
||||
# end
|
||||
#
|
||||
# alternative.part "text/plain" do |plain|
|
||||
# plain.body = render_message("welcome_email.text.plain", :message => "text content")
|
||||
# end
|
||||
#
|
||||
# end
|
||||
#
|
||||
# attachment :content_type => "image/png",
|
||||
# :body => File.read(File.join(RAILS_ROOT, 'public/images/rails.png'))
|
||||
#
|
||||
# attachment "application/pdf" do |a|
|
||||
# a.body = File.read('/Users/mikel/Code/mail/spec/fixtures/attachments/test.pdf')
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# = Configuration options
|
||||
#
|
||||
@@ -278,7 +311,7 @@ module ActionMailer #:nodoc:
|
||||
@@raise_delivery_errors = true
|
||||
cattr_accessor :raise_delivery_errors
|
||||
|
||||
superclass_delegating_accessor :delivery_method
|
||||
class_attribute :delivery_method
|
||||
self.delivery_method = :smtp
|
||||
|
||||
@@perform_deliveries = true
|
||||
@@ -543,6 +576,7 @@ module ActionMailer #:nodoc:
|
||||
@headers ||= {}
|
||||
@body ||= {}
|
||||
@mime_version = @@default_mime_version.dup if @@default_mime_version
|
||||
@sent_on ||= Time.now
|
||||
end
|
||||
|
||||
def render_message(method_name, body)
|
||||
@@ -592,7 +626,7 @@ module ActionMailer #:nodoc:
|
||||
end
|
||||
|
||||
def template_path
|
||||
"#{template_root}/#{mailer_name}"
|
||||
File.join(template_root, mailer_name)
|
||||
end
|
||||
|
||||
def initialize_template_class(assigns)
|
||||
@@ -674,7 +708,7 @@ module ActionMailer #:nodoc:
|
||||
def perform_delivery_smtp(mail)
|
||||
destinations = mail.destinations
|
||||
mail.ready_to_send
|
||||
sender = (mail['return-path'] && mail['return-path'].spec) || mail.from
|
||||
sender = (mail['return-path'] && mail['return-path'].spec) || Array(mail.from).first
|
||||
|
||||
smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
|
||||
smtp.enable_starttls_auto if smtp_settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
|
||||
|
||||
@@ -105,7 +105,7 @@ module ActionMailer
|
||||
private
|
||||
# Extend the template class instance with our controller's helper module.
|
||||
def initialize_template_class_with_helper(assigns)
|
||||
returning(template = initialize_template_class_without_helper(assigns)) do
|
||||
initialize_template_class_without_helper(assigns).tap do |template|
|
||||
template.extend self.class.master_helper_module
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# encoding: us-ascii
|
||||
module ActionMailer
|
||||
module Quoting #:nodoc:
|
||||
# Convert the given text into quoted printable format, with an instruction
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
require 'tmail/version'
|
||||
require 'tmail/mail'
|
||||
require 'tmail/mailbox'
|
||||
require 'tmail/core_extensions'
|
||||
require 'tmail/net'
|
||||
@@ -1,426 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Address handling class
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
require 'tmail/encode'
|
||||
require 'tmail/parser'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
# = Class Address
|
||||
#
|
||||
# Provides a complete handling library for email addresses. Can parse a string of an
|
||||
# address directly or take in preformatted addresses themselves. Allows you to add
|
||||
# and remove phrases from the front of the address and provides a compare function for
|
||||
# email addresses.
|
||||
#
|
||||
# == Parsing and Handling a Valid Address:
|
||||
#
|
||||
# Just pass the email address in as a string to Address.parse:
|
||||
#
|
||||
# email = TMail::Address.parse('Mikel Lindsaar <mikel@lindsaar.net>)
|
||||
# #=> #<TMail::Address mikel@lindsaar.net>
|
||||
# email.address
|
||||
# #=> "mikel@lindsaar.net"
|
||||
# email.local
|
||||
# #=> "mikel"
|
||||
# email.domain
|
||||
# #=> "lindsaar.net"
|
||||
# email.name # Aliased as phrase as well
|
||||
# #=> "Mikel Lindsaar"
|
||||
#
|
||||
# == Detecting an Invalid Address
|
||||
#
|
||||
# If you want to check the syntactical validity of an email address, just pass it to
|
||||
# Address.parse and catch any SyntaxError:
|
||||
#
|
||||
# begin
|
||||
# TMail::Mail.parse("mikel 2@@@@@ me .com")
|
||||
# rescue TMail::SyntaxError
|
||||
# puts("Invalid Email Address Detected")
|
||||
# else
|
||||
# puts("Address is valid")
|
||||
# end
|
||||
# #=> "Invalid Email Address Detected"
|
||||
class Address
|
||||
|
||||
include TextUtils #:nodoc:
|
||||
|
||||
# Sometimes you need to parse an address, TMail can do it for you and provide you with
|
||||
# a fairly robust method of detecting a valid address.
|
||||
#
|
||||
# Takes in a string, returns a TMail::Address object.
|
||||
#
|
||||
# Raises a TMail::SyntaxError on invalid email format
|
||||
def Address.parse( str )
|
||||
Parser.parse :ADDRESS, special_quote_address(str)
|
||||
end
|
||||
|
||||
def Address.special_quote_address(str) #:nodoc:
|
||||
# Takes a string which is an address and adds quotation marks to special
|
||||
# edge case methods that the RACC parser can not handle.
|
||||
#
|
||||
# Right now just handles two edge cases:
|
||||
#
|
||||
# Full stop as the last character of the display name:
|
||||
# Mikel L. <mikel@me.com>
|
||||
# Returns:
|
||||
# "Mikel L." <mikel@me.com>
|
||||
#
|
||||
# Unquoted @ symbol in the display name:
|
||||
# mikel@me.com <mikel@me.com>
|
||||
# Returns:
|
||||
# "mikel@me.com" <mikel@me.com>
|
||||
#
|
||||
# Any other address not matching these patterns just gets returned as is.
|
||||
case
|
||||
# This handles the missing "" in an older version of Apple Mail.app
|
||||
# around the display name when the display name contains a '@'
|
||||
# like 'mikel@me.com <mikel@me.com>'
|
||||
# Just quotes it to: '"mikel@me.com" <mikel@me.com>'
|
||||
when str =~ /\A([^"].+@.+[^"])\s(<.*?>)\Z/
|
||||
return "\"#{$1}\" #{$2}"
|
||||
# This handles cases where 'Mikel A. <mikel@me.com>' which is a trailing
|
||||
# full stop before the address section. Just quotes it to
|
||||
# '"Mikel A. <mikel@me.com>"
|
||||
when str =~ /\A(.*?\.)\s(<.*?>)\Z/
|
||||
return "\"#{$1}\" #{$2}"
|
||||
else
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
def address_group? #:nodoc:
|
||||
false
|
||||
end
|
||||
|
||||
# Address.new(local, domain)
|
||||
#
|
||||
# Accepts:
|
||||
#
|
||||
# * local - Left of the at symbol
|
||||
#
|
||||
# * domain - Array of the domain split at the periods.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# Address.new("mikel", ["lindsaar", "net"])
|
||||
# #=> "#<TMail::Address mikel@lindsaar.net>"
|
||||
def initialize( local, domain )
|
||||
if domain
|
||||
domain.each do |s|
|
||||
raise SyntaxError, 'empty word in domain' if s.empty?
|
||||
end
|
||||
end
|
||||
|
||||
# This is to catch an unquoted "@" symbol in the local part of the
|
||||
# address. Handles addresses like <"@"@me.com> and makes sure they
|
||||
# stay like <"@"@me.com> (previously were becoming <@@me.com>)
|
||||
if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/)
|
||||
@local = "\"#{local.join}\""
|
||||
else
|
||||
@local = local
|
||||
end
|
||||
|
||||
@domain = domain
|
||||
@name = nil
|
||||
@routes = []
|
||||
end
|
||||
|
||||
# Provides the name or 'phrase' of the email address.
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Address.parse("Mikel Lindsaar <mikel@lindsaar.net>")
|
||||
# email.name
|
||||
# #=> "Mikel Lindsaar"
|
||||
def name
|
||||
@name
|
||||
end
|
||||
|
||||
# Setter method for the name or phrase of the email
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Address.parse("mikel@lindsaar.net")
|
||||
# email.name
|
||||
# #=> nil
|
||||
# email.name = "Mikel Lindsaar"
|
||||
# email.to_s
|
||||
# #=> "Mikel Lindsaar <mikel@me.com>"
|
||||
def name=( str )
|
||||
@name = str
|
||||
@name = nil if str and str.empty?
|
||||
end
|
||||
|
||||
#:stopdoc:
|
||||
alias phrase name
|
||||
alias phrase= name=
|
||||
#:startdoc:
|
||||
|
||||
# This is still here from RFC 822, and is now obsolete per RFC2822 Section 4.
|
||||
#
|
||||
# "When interpreting addresses, the route portion SHOULD be ignored."
|
||||
#
|
||||
# It is still here, so you can access it.
|
||||
#
|
||||
# Routes return the route portion at the front of the email address, if any.
|
||||
#
|
||||
# For Example:
|
||||
# email = TMail::Address.parse( "<@sa,@another:Mikel@me.com>")
|
||||
# => #<TMail::Address Mikel@me.com>
|
||||
# email.to_s
|
||||
# => "<@sa,@another:Mikel@me.com>"
|
||||
# email.routes
|
||||
# => ["sa", "another"]
|
||||
def routes
|
||||
@routes
|
||||
end
|
||||
|
||||
def inspect #:nodoc:
|
||||
"#<#{self.class} #{address()}>"
|
||||
end
|
||||
|
||||
# Returns the local part of the email address
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Address.parse("mikel@lindsaar.net")
|
||||
# email.local
|
||||
# #=> "mikel"
|
||||
def local
|
||||
return nil unless @local
|
||||
return '""' if @local.size == 1 and @local[0].empty?
|
||||
# Check to see if it is an array before trying to map it
|
||||
if @local.respond_to?(:map)
|
||||
@local.map {|i| quote_atom(i) }.join('.')
|
||||
else
|
||||
quote_atom(@local)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the domain part of the email address
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Address.parse("mikel@lindsaar.net")
|
||||
# email.local
|
||||
# #=> "lindsaar.net"
|
||||
def domain
|
||||
return nil unless @domain
|
||||
join_domain(@domain)
|
||||
end
|
||||
|
||||
# Returns the full specific address itself
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Address.parse("mikel@lindsaar.net")
|
||||
# email.address
|
||||
# #=> "mikel@lindsaar.net"
|
||||
def spec
|
||||
s = self.local
|
||||
d = self.domain
|
||||
if s and d
|
||||
s + '@' + d
|
||||
else
|
||||
s
|
||||
end
|
||||
end
|
||||
|
||||
alias address spec
|
||||
|
||||
# Provides == function to the email. Only checks the actual address
|
||||
# and ignores the name/phrase component
|
||||
#
|
||||
# For Example
|
||||
#
|
||||
# addr1 = TMail::Address.parse("My Address <mikel@lindsaar.net>")
|
||||
# #=> "#<TMail::Address mikel@lindsaar.net>"
|
||||
# addr2 = TMail::Address.parse("Another <mikel@lindsaar.net>")
|
||||
# #=> "#<TMail::Address mikel@lindsaar.net>"
|
||||
# addr1 == addr2
|
||||
# #=> true
|
||||
def ==( other )
|
||||
other.respond_to? :spec and self.spec == other.spec
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
# Provides a unique hash value for this record against the local and domain
|
||||
# parts, ignores the name/phrase value
|
||||
#
|
||||
# email = TMail::Address.parse("mikel@lindsaar.net")
|
||||
# email.hash
|
||||
# #=> 18767598
|
||||
def hash
|
||||
@local.hash ^ @domain.hash
|
||||
end
|
||||
|
||||
# Duplicates a TMail::Address object returning the duplicate
|
||||
#
|
||||
# addr1 = TMail::Address.parse("mikel@lindsaar.net")
|
||||
# addr2 = addr1.dup
|
||||
# addr1.id == addr2.id
|
||||
# #=> false
|
||||
def dup
|
||||
obj = self.class.new(@local.dup, @domain.dup)
|
||||
obj.name = @name.dup if @name
|
||||
obj.routes.replace @routes
|
||||
obj
|
||||
end
|
||||
|
||||
include StrategyInterface #:nodoc:
|
||||
|
||||
def accept( strategy, dummy1 = nil, dummy2 = nil ) #:nodoc:
|
||||
unless @local
|
||||
strategy.meta '<>' # empty return-path
|
||||
return
|
||||
end
|
||||
|
||||
spec_p = (not @name and @routes.empty?)
|
||||
if @name
|
||||
strategy.phrase @name
|
||||
strategy.space
|
||||
end
|
||||
tmp = spec_p ? '' : '<'
|
||||
unless @routes.empty?
|
||||
tmp << @routes.map {|i| '@' + i }.join(',') << ':'
|
||||
end
|
||||
tmp << self.spec
|
||||
tmp << '>' unless spec_p
|
||||
strategy.meta tmp
|
||||
strategy.lwsp ''
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class AddressGroup
|
||||
|
||||
include Enumerable
|
||||
|
||||
def address_group?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize( name, addrs )
|
||||
@name = name
|
||||
@addresses = addrs
|
||||
end
|
||||
|
||||
attr_reader :name
|
||||
|
||||
def ==( other )
|
||||
other.respond_to? :to_a and @addresses == other.to_a
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
map {|i| i.hash }.hash
|
||||
end
|
||||
|
||||
def []( idx )
|
||||
@addresses[idx]
|
||||
end
|
||||
|
||||
def size
|
||||
@addresses.size
|
||||
end
|
||||
|
||||
def empty?
|
||||
@addresses.empty?
|
||||
end
|
||||
|
||||
def each( &block )
|
||||
@addresses.each(&block)
|
||||
end
|
||||
|
||||
def to_a
|
||||
@addresses.dup
|
||||
end
|
||||
|
||||
alias to_ary to_a
|
||||
|
||||
def include?( a )
|
||||
@addresses.include? a
|
||||
end
|
||||
|
||||
def flatten
|
||||
set = []
|
||||
@addresses.each do |a|
|
||||
if a.respond_to? :flatten
|
||||
set.concat a.flatten
|
||||
else
|
||||
set.push a
|
||||
end
|
||||
end
|
||||
set
|
||||
end
|
||||
|
||||
def each_address( &block )
|
||||
flatten.each(&block)
|
||||
end
|
||||
|
||||
def add( a )
|
||||
@addresses.push a
|
||||
end
|
||||
|
||||
alias push add
|
||||
|
||||
def delete( a )
|
||||
@addresses.delete a
|
||||
end
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def accept( strategy, dummy1 = nil, dummy2 = nil )
|
||||
strategy.phrase @name
|
||||
strategy.meta ':'
|
||||
strategy.space
|
||||
first = true
|
||||
each do |mbox|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.meta ','
|
||||
end
|
||||
strategy.space
|
||||
mbox.accept strategy
|
||||
end
|
||||
strategy.meta ';'
|
||||
strategy.lwsp ''
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
||||
@@ -1,46 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Attachment handling file
|
||||
|
||||
=end
|
||||
|
||||
require 'stringio'
|
||||
|
||||
module TMail
|
||||
class Attachment < StringIO
|
||||
attr_accessor :original_filename, :content_type
|
||||
end
|
||||
|
||||
class Mail
|
||||
def has_attachments?
|
||||
multipart? && parts.any? { |part| attachment?(part) }
|
||||
end
|
||||
|
||||
def attachment?(part)
|
||||
part.disposition_is_attachment? || part.content_type_is_text?
|
||||
end
|
||||
|
||||
def attachments
|
||||
if multipart?
|
||||
parts.collect { |part|
|
||||
if part.multipart?
|
||||
part.attachments
|
||||
elsif attachment?(part)
|
||||
content = part.body # unquoted automatically by TMail#body
|
||||
file_name = (part['content-location'] &&
|
||||
part['content-location'].body) ||
|
||||
part.sub_header("content-type", "name") ||
|
||||
part.sub_header("content-disposition", "filename")
|
||||
|
||||
next if file_name.blank? || content.blank?
|
||||
|
||||
attachment = Attachment.new(content)
|
||||
attachment.original_filename = file_name.strip
|
||||
attachment.content_type = part.content_type
|
||||
attachment
|
||||
end
|
||||
}.flatten.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,46 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
#:stopdoc:
|
||||
module TMail
|
||||
module Base64
|
||||
|
||||
module_function
|
||||
|
||||
def folding_encode( str, eol = "\n", limit = 60 )
|
||||
[str].pack('m')
|
||||
end
|
||||
|
||||
def encode( str )
|
||||
[str].pack('m').tr( "\r\n", '' )
|
||||
end
|
||||
|
||||
def decode( str, strict = false )
|
||||
str.unpack('m').first
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
#:startdoc:
|
||||
@@ -1,41 +0,0 @@
|
||||
#:stopdoc:
|
||||
unless Enumerable.method_defined?(:map)
|
||||
module Enumerable #:nodoc:
|
||||
alias map collect
|
||||
end
|
||||
end
|
||||
|
||||
unless Enumerable.method_defined?(:select)
|
||||
module Enumerable #:nodoc:
|
||||
alias select find_all
|
||||
end
|
||||
end
|
||||
|
||||
unless Enumerable.method_defined?(:reject)
|
||||
module Enumerable #:nodoc:
|
||||
def reject
|
||||
result = []
|
||||
each do |i|
|
||||
result.push i unless yield(i)
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless Enumerable.method_defined?(:sort_by)
|
||||
module Enumerable #:nodoc:
|
||||
def sort_by
|
||||
map {|i| [yield(i), i] }.sort.map {|val, i| i }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless File.respond_to?(:read)
|
||||
def File.read(fname) #:nodoc:
|
||||
File.open(fname) {|f|
|
||||
return f.read
|
||||
}
|
||||
end
|
||||
end
|
||||
#:startdoc:
|
||||
@@ -1,67 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
#:stopdoc:
|
||||
module TMail
|
||||
|
||||
class Config
|
||||
|
||||
def initialize( strict )
|
||||
@strict_parse = strict
|
||||
@strict_base64decode = strict
|
||||
end
|
||||
|
||||
def strict_parse?
|
||||
@strict_parse
|
||||
end
|
||||
|
||||
attr_writer :strict_parse
|
||||
|
||||
def strict_base64decode?
|
||||
@strict_base64decode
|
||||
end
|
||||
|
||||
attr_writer :strict_base64decode
|
||||
|
||||
def new_body_port( mail )
|
||||
StringPort.new
|
||||
end
|
||||
|
||||
alias new_preamble_port new_body_port
|
||||
alias new_part_port new_body_port
|
||||
|
||||
end
|
||||
|
||||
DEFAULT_CONFIG = Config.new(false)
|
||||
DEFAULT_STRICT_CONFIG = Config.new(true)
|
||||
|
||||
def Config.to_config( arg )
|
||||
return DEFAULT_STRICT_CONFIG if arg == true
|
||||
return DEFAULT_CONFIG if arg == false
|
||||
arg or DEFAULT_CONFIG
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
@@ -1,63 +0,0 @@
|
||||
#:stopdoc:
|
||||
unless Object.respond_to?(:blank?)
|
||||
class Object
|
||||
# Check first to see if we are in a Rails environment, no need to
|
||||
# define these methods if we are
|
||||
|
||||
# An object is blank if it's nil, empty, or a whitespace string.
|
||||
# For example, "", " ", nil, [], and {} are blank.
|
||||
#
|
||||
# This simplifies
|
||||
# if !address.nil? && !address.empty?
|
||||
# to
|
||||
# if !address.blank?
|
||||
def blank?
|
||||
if respond_to?(:empty?) && respond_to?(:strip)
|
||||
empty? or strip.empty?
|
||||
elsif respond_to?(:empty?)
|
||||
empty?
|
||||
else
|
||||
!self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class NilClass
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class FalseClass
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class Array
|
||||
alias_method :blank?, :empty?
|
||||
end
|
||||
|
||||
class Hash
|
||||
alias_method :blank?, :empty?
|
||||
end
|
||||
|
||||
class String
|
||||
def blank?
|
||||
empty? || strip.empty?
|
||||
end
|
||||
end
|
||||
|
||||
class Numeric
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
#:startdoc:
|
||||
@@ -1,581 +0,0 @@
|
||||
#--
|
||||
# = COPYRIGHT:
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
#:stopdoc:
|
||||
require 'nkf'
|
||||
require 'tmail/base64'
|
||||
require 'tmail/stringio'
|
||||
require 'tmail/utils'
|
||||
#:startdoc:
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
#:stopdoc:
|
||||
class << self
|
||||
attr_accessor :KCODE
|
||||
end
|
||||
self.KCODE = 'NONE'
|
||||
|
||||
module StrategyInterface
|
||||
|
||||
def create_dest( obj )
|
||||
case obj
|
||||
when nil
|
||||
StringOutput.new
|
||||
when String
|
||||
StringOutput.new(obj)
|
||||
when IO, StringOutput
|
||||
obj
|
||||
else
|
||||
raise TypeError, 'cannot handle this type of object for dest'
|
||||
end
|
||||
end
|
||||
module_function :create_dest
|
||||
|
||||
#:startdoc:
|
||||
# Returns the TMail object encoded and ready to be sent via SMTP etc.
|
||||
# You should call this before you are packaging up your email to
|
||||
# correctly escape all the values that need escaping in the email, line
|
||||
# wrap the email etc.
|
||||
#
|
||||
# It is also a good idea to call this before you marshal or serialize
|
||||
# a TMail object.
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Load(my_email_file)
|
||||
# email_to_send = email.encoded
|
||||
def encoded( eol = "\r\n", charset = 'j', dest = nil )
|
||||
accept_strategy Encoder, eol, charset, dest
|
||||
end
|
||||
|
||||
# Returns the TMail object decoded and ready to be used by you, your
|
||||
# program etc.
|
||||
#
|
||||
# You should call this before you are packaging up your email to
|
||||
# correctly escape all the values that need escaping in the email, line
|
||||
# wrap the email etc.
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email = TMail::Load(my_email_file)
|
||||
# email_to_send = email.encoded
|
||||
def decoded( eol = "\n", charset = 'e', dest = nil )
|
||||
# Turn the E-Mail into a string and return it with all
|
||||
# encoded characters decoded. alias for to_s
|
||||
accept_strategy Decoder, eol, charset, dest
|
||||
end
|
||||
|
||||
alias to_s decoded
|
||||
|
||||
def accept_strategy( klass, eol, charset, dest = nil ) #:nodoc:
|
||||
dest ||= ''
|
||||
accept klass.new( create_dest(dest), charset, eol )
|
||||
dest
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#:stopdoc:
|
||||
|
||||
###
|
||||
### MIME B encoding decoder
|
||||
###
|
||||
|
||||
class Decoder
|
||||
|
||||
include TextUtils
|
||||
|
||||
encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
|
||||
ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
|
||||
|
||||
OUTPUT_ENCODING = {
|
||||
'EUC' => 'e',
|
||||
'SJIS' => 's',
|
||||
}
|
||||
|
||||
def self.decode( str, encoding = nil )
|
||||
encoding ||= (OUTPUT_ENCODING[TMail.KCODE] || 'j')
|
||||
opt = '-mS' + encoding
|
||||
str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
|
||||
end
|
||||
|
||||
def initialize( dest, encoding = nil, eol = "\n" )
|
||||
@f = StrategyInterface.create_dest(dest)
|
||||
@encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
|
||||
@eol = eol
|
||||
end
|
||||
|
||||
def decode( str )
|
||||
self.class.decode(str, @encoding)
|
||||
end
|
||||
private :decode
|
||||
|
||||
def terminate
|
||||
end
|
||||
|
||||
def header_line( str )
|
||||
@f << decode(str)
|
||||
end
|
||||
|
||||
def header_name( nm )
|
||||
@f << nm << ': '
|
||||
end
|
||||
|
||||
def header_body( str )
|
||||
@f << decode(str)
|
||||
end
|
||||
|
||||
def space
|
||||
@f << ' '
|
||||
end
|
||||
|
||||
alias spc space
|
||||
|
||||
def lwsp( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
def meta( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
def text( str )
|
||||
@f << decode(str)
|
||||
end
|
||||
|
||||
def phrase( str )
|
||||
@f << quote_phrase(decode(str))
|
||||
end
|
||||
|
||||
def kv_pair( k, v )
|
||||
v = dquote(v) unless token_safe?(v)
|
||||
@f << k << '=' << v
|
||||
end
|
||||
|
||||
def puts( str = nil )
|
||||
@f << str if str
|
||||
@f << @eol
|
||||
end
|
||||
|
||||
def write( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### MIME B-encoding encoder
|
||||
###
|
||||
|
||||
#
|
||||
# FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
|
||||
#
|
||||
class Encoder
|
||||
|
||||
include TextUtils
|
||||
|
||||
BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
|
||||
|
||||
def Encoder.encode( str )
|
||||
e = new()
|
||||
e.header_body str
|
||||
e.terminate
|
||||
e.dest.string
|
||||
end
|
||||
|
||||
SPACER = "\t"
|
||||
MAX_LINE_LEN = 78
|
||||
RFC_2822_MAX_LENGTH = 998
|
||||
|
||||
OPTIONS = {
|
||||
'EUC' => '-Ej -m0',
|
||||
'SJIS' => '-Sj -m0',
|
||||
'UTF8' => nil, # FIXME
|
||||
'NONE' => nil
|
||||
}
|
||||
|
||||
def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
|
||||
@f = StrategyInterface.create_dest(dest)
|
||||
@opt = OPTIONS[TMail.KCODE]
|
||||
@eol = eol
|
||||
@folded = false
|
||||
@preserve_quotes = true
|
||||
reset
|
||||
end
|
||||
|
||||
def preserve_quotes=( bool )
|
||||
@preserve_quotes
|
||||
end
|
||||
|
||||
def preserve_quotes
|
||||
@preserve_quotes
|
||||
end
|
||||
|
||||
def normalize_encoding( str )
|
||||
if @opt
|
||||
then NKF.nkf(@opt, str)
|
||||
else str
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
@text = ''
|
||||
@lwsp = ''
|
||||
@curlen = 0
|
||||
end
|
||||
|
||||
def terminate
|
||||
add_lwsp ''
|
||||
reset
|
||||
end
|
||||
|
||||
def dest
|
||||
@f
|
||||
end
|
||||
|
||||
def puts( str = nil )
|
||||
@f << str if str
|
||||
@f << @eol
|
||||
end
|
||||
|
||||
def write( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
#
|
||||
# add
|
||||
#
|
||||
|
||||
def header_line( line )
|
||||
scanadd line
|
||||
end
|
||||
|
||||
def header_name( name )
|
||||
add_text name.split(/-/).map {|i| i.capitalize }.join('-')
|
||||
add_text ':'
|
||||
add_lwsp ' '
|
||||
end
|
||||
|
||||
def header_body( str )
|
||||
scanadd normalize_encoding(str)
|
||||
end
|
||||
|
||||
def space
|
||||
add_lwsp ' '
|
||||
end
|
||||
|
||||
alias spc space
|
||||
|
||||
def lwsp( str )
|
||||
add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
|
||||
end
|
||||
|
||||
def meta( str )
|
||||
add_text str
|
||||
end
|
||||
|
||||
def text( str )
|
||||
scanadd normalize_encoding(str)
|
||||
end
|
||||
|
||||
def phrase( str )
|
||||
str = normalize_encoding(str)
|
||||
if CONTROL_CHAR === str
|
||||
scanadd str
|
||||
else
|
||||
add_text quote_phrase(str)
|
||||
end
|
||||
end
|
||||
|
||||
# FIXME: implement line folding
|
||||
#
|
||||
def kv_pair( k, v )
|
||||
return if v.nil?
|
||||
v = normalize_encoding(v)
|
||||
if token_safe?(v)
|
||||
add_text k + '=' + v
|
||||
elsif not CONTROL_CHAR === v
|
||||
add_text k + '=' + quote_token(v)
|
||||
else
|
||||
# apply RFC2231 encoding
|
||||
kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
|
||||
add_text kv
|
||||
end
|
||||
end
|
||||
|
||||
def encode_value( str )
|
||||
str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scanadd( str, force = false )
|
||||
types = ''
|
||||
strs = []
|
||||
if str.respond_to?(:encoding)
|
||||
enc = str.encoding
|
||||
str.force_encoding(Encoding::ASCII_8BIT)
|
||||
end
|
||||
until str.empty?
|
||||
if m = /\A[^\e\t\r\n ]+/.match(str)
|
||||
types << (force ? 'j' : 'a')
|
||||
if str.respond_to?(:encoding)
|
||||
strs.push m[0].force_encoding(enc)
|
||||
else
|
||||
strs.push m[0]
|
||||
end
|
||||
elsif m = /\A[\t\r\n ]+/.match(str)
|
||||
types << 's'
|
||||
if str.respond_to?(:encoding)
|
||||
strs.push m[0].force_encoding(enc)
|
||||
else
|
||||
strs.push m[0]
|
||||
end
|
||||
|
||||
elsif m = /\A\e../.match(str)
|
||||
esc = m[0]
|
||||
str = m.post_match
|
||||
if esc != "\e(B" and m = /\A[^\e]+/.match(str)
|
||||
types << 'j'
|
||||
if str.respond_to?(:encoding)
|
||||
strs.push m[0].force_encoding(enc)
|
||||
else
|
||||
strs.push m[0]
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
raise 'TMail FATAL: encoder scan fail'
|
||||
end
|
||||
(str = m.post_match) unless m.nil?
|
||||
end
|
||||
|
||||
do_encode types, strs
|
||||
end
|
||||
|
||||
def do_encode( types, strs )
|
||||
#
|
||||
# result : (A|E)(S(A|E))*
|
||||
# E : W(SW)*
|
||||
# W : (J|A)+ but must contain J # (J|A)*J(J|A)*
|
||||
# A : <<A character string not to be encoded>>
|
||||
# J : <<A character string to be encoded>>
|
||||
# S : <<LWSP>>
|
||||
#
|
||||
# An encoding unit is `E'.
|
||||
# Input (parameter `types') is (J|A)(J|A|S)*(J|A)
|
||||
#
|
||||
if BENCODE_DEBUG
|
||||
puts
|
||||
puts '-- do_encode ------------'
|
||||
puts types.split(//).join(' ')
|
||||
p strs
|
||||
end
|
||||
|
||||
e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
|
||||
|
||||
while m = e.match(types)
|
||||
pre = m.pre_match
|
||||
concat_A_S pre, strs[0, pre.size] unless pre.empty?
|
||||
concat_E m[0], strs[m.begin(0) ... m.end(0)]
|
||||
types = m.post_match
|
||||
strs.slice! 0, m.end(0)
|
||||
end
|
||||
concat_A_S types, strs
|
||||
end
|
||||
|
||||
def concat_A_S( types, strs )
|
||||
if RUBY_VERSION < '1.9'
|
||||
a = ?a; s = ?s
|
||||
else
|
||||
a = 'a'.ord; s = 's'.ord
|
||||
end
|
||||
i = 0
|
||||
types.each_byte do |t|
|
||||
case t
|
||||
when a then add_text strs[i]
|
||||
when s then add_lwsp strs[i]
|
||||
else
|
||||
raise "TMail FATAL: unknown flag: #{t.chr}"
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
METHOD_ID = {
|
||||
?j => :extract_J,
|
||||
?e => :extract_E,
|
||||
?a => :extract_A,
|
||||
?s => :extract_S
|
||||
}
|
||||
|
||||
def concat_E( types, strs )
|
||||
if BENCODE_DEBUG
|
||||
puts '---- concat_E'
|
||||
puts "types=#{types.split(//).join(' ')}"
|
||||
puts "strs =#{strs.inspect}"
|
||||
end
|
||||
|
||||
flush() unless @text.empty?
|
||||
|
||||
chunk = ''
|
||||
strs.each_with_index do |s,i|
|
||||
mid = METHOD_ID[types[i]]
|
||||
until s.empty?
|
||||
unless c = __send__(mid, chunk.size, s)
|
||||
add_with_encode chunk unless chunk.empty?
|
||||
flush
|
||||
chunk = ''
|
||||
fold
|
||||
c = __send__(mid, 0, s)
|
||||
raise 'TMail FATAL: extract fail' unless c
|
||||
end
|
||||
chunk << c
|
||||
end
|
||||
end
|
||||
add_with_encode chunk unless chunk.empty?
|
||||
end
|
||||
|
||||
def extract_J( chunksize, str )
|
||||
size = max_bytes(chunksize, str.size) - 6
|
||||
size = (size % 2 == 0) ? (size) : (size - 1)
|
||||
return nil if size <= 0
|
||||
if str.respond_to?(:encoding)
|
||||
enc = str.encoding
|
||||
str.force_encoding(Encoding::ASCII_8BIT)
|
||||
"\e$B#{str.slice!(0, size)}\e(B".force_encoding(enc)
|
||||
else
|
||||
"\e$B#{str.slice!(0, size)}\e(B"
|
||||
end
|
||||
end
|
||||
|
||||
def extract_A( chunksize, str )
|
||||
size = max_bytes(chunksize, str.size)
|
||||
return nil if size <= 0
|
||||
str.slice!(0, size)
|
||||
end
|
||||
|
||||
alias extract_S extract_A
|
||||
|
||||
def max_bytes( chunksize, ssize )
|
||||
(restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
|
||||
end
|
||||
|
||||
#
|
||||
# free length buffer
|
||||
#
|
||||
|
||||
def add_text( str )
|
||||
@text << str
|
||||
# puts '---- text -------------------------------------'
|
||||
# puts "+ #{str.inspect}"
|
||||
# puts "txt >>>#{@text.inspect}<<<"
|
||||
end
|
||||
|
||||
def add_with_encode( str )
|
||||
@text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
|
||||
end
|
||||
|
||||
def add_lwsp( lwsp )
|
||||
# puts '---- lwsp -------------------------------------'
|
||||
# puts "+ #{lwsp.inspect}"
|
||||
fold if restsize() <= 0
|
||||
flush(@folded)
|
||||
@lwsp = lwsp
|
||||
end
|
||||
|
||||
def flush(folded = false)
|
||||
# puts '---- flush ----'
|
||||
# puts "spc >>>#{@lwsp.inspect}<<<"
|
||||
# puts "txt >>>#{@text.inspect}<<<"
|
||||
@f << @lwsp << @text
|
||||
if folded
|
||||
@curlen = 0
|
||||
else
|
||||
@curlen += (@lwsp.size + @text.size)
|
||||
end
|
||||
@text = ''
|
||||
@lwsp = ''
|
||||
end
|
||||
|
||||
def fold
|
||||
# puts '---- fold ----'
|
||||
unless @f.string =~ /^.*?:$/
|
||||
@f << @eol
|
||||
@lwsp = SPACER
|
||||
else
|
||||
fold_header
|
||||
@folded = true
|
||||
end
|
||||
@curlen = 0
|
||||
end
|
||||
|
||||
def fold_header
|
||||
# Called because line is too long - so we need to wrap.
|
||||
# First look for whitespace in the text
|
||||
# if it has text, fold there
|
||||
# check the remaining text, if too long, fold again
|
||||
# if it doesn't, then don't fold unless the line goes beyond 998 chars
|
||||
|
||||
# Check the text to see if there is whitespace, or if not
|
||||
@wrapped_text = []
|
||||
until @text.blank?
|
||||
fold_the_string
|
||||
end
|
||||
@text = @wrapped_text.join("#{@eol}#{SPACER}")
|
||||
end
|
||||
|
||||
def fold_the_string
|
||||
whitespace_location = @text =~ /\s/ || @text.length
|
||||
# Is the location of the whitespace shorter than the RCF_2822_MAX_LENGTH?
|
||||
# if there is no whitespace in the string, then this
|
||||
unless mazsize(whitespace_location) <= 0
|
||||
@text.strip!
|
||||
@wrapped_text << @text.slice!(0...whitespace_location)
|
||||
# If it is not less, we have to wrap it destructively
|
||||
else
|
||||
slice_point = RFC_2822_MAX_LENGTH - @curlen - @lwsp.length
|
||||
@text.strip!
|
||||
@wrapped_text << @text.slice!(0...slice_point)
|
||||
end
|
||||
end
|
||||
|
||||
def restsize
|
||||
MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
|
||||
end
|
||||
|
||||
def mazsize(whitespace_location)
|
||||
# Per RFC2822, the maximum length of a line is 998 chars
|
||||
RFC_2822_MAX_LENGTH - (@curlen + @lwsp.size + whitespace_location)
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
end # module TMail
|
||||
@@ -1,960 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
require 'tmail/encode'
|
||||
require 'tmail/address'
|
||||
require 'tmail/parser'
|
||||
require 'tmail/config'
|
||||
require 'tmail/utils'
|
||||
|
||||
#:startdoc:
|
||||
module TMail
|
||||
|
||||
# Provides methods to handle and manipulate headers in the email
|
||||
class HeaderField
|
||||
|
||||
include TextUtils
|
||||
|
||||
class << self
|
||||
|
||||
alias newobj new
|
||||
|
||||
def new( name, body, conf = DEFAULT_CONFIG )
|
||||
klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
|
||||
klass.newobj body, conf
|
||||
end
|
||||
|
||||
# Returns a HeaderField object matching the header you specify in the "name" param.
|
||||
# Requires an initialized TMail::Port to be passed in.
|
||||
#
|
||||
# The method searches the header of the Port you pass into it to find a match on
|
||||
# the header line you pass. Once a match is found, it will unwrap the matching line
|
||||
# as needed to return an initialized HeaderField object.
|
||||
#
|
||||
# If you want to get the Envelope sender of the email object, pass in "EnvelopeSender",
|
||||
# if you want the From address of the email itself, pass in 'From'.
|
||||
#
|
||||
# This is because a mailbox doesn't have the : after the From that designates the
|
||||
# beginning of the envelope sender (which can be different to the from address of
|
||||
# the email)
|
||||
#
|
||||
# Other fields can be passed as normal, "Reply-To", "Received" etc.
|
||||
#
|
||||
# Note: Change of behaviour in 1.2.1 => returns nil if it does not find the specified
|
||||
# header field, otherwise returns an instantiated object of the correct header class
|
||||
#
|
||||
# For example:
|
||||
# port = TMail::FilePort.new("/test/fixtures/raw_email_simple")
|
||||
# h = TMail::HeaderField.new_from_port(port, "From")
|
||||
# h.addrs.to_s #=> "Mikel Lindsaar <mikel@nowhere.com>"
|
||||
# h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
|
||||
# h.addrs.to_s #=> "mike@anotherplace.com.au"
|
||||
# h = TMail::HeaderField.new_from_port(port, "SomeWeirdHeaderField")
|
||||
# h #=> nil
|
||||
def new_from_port( port, name, conf = DEFAULT_CONFIG )
|
||||
if name == "EnvelopeSender"
|
||||
name = "From"
|
||||
re = Regexp.new('\A(From) ', 'i')
|
||||
else
|
||||
re = Regexp.new('\A(' + Regexp.quote(name) + '):', 'i')
|
||||
end
|
||||
str = nil
|
||||
port.ropen {|f|
|
||||
f.each do |line|
|
||||
if m = re.match(line) then str = m.post_match.strip
|
||||
elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
|
||||
elsif /\A-*\s*\z/ === line then break
|
||||
elsif str then break
|
||||
end
|
||||
end
|
||||
}
|
||||
new(name, str, Config.to_config(conf)) if str
|
||||
end
|
||||
|
||||
def internal_new( name, conf )
|
||||
FNAME_TO_CLASS[name].newobj('', conf, true)
|
||||
end
|
||||
|
||||
end # class << self
|
||||
|
||||
def initialize( body, conf, intern = false )
|
||||
@body = body
|
||||
@config = conf
|
||||
|
||||
@illegal = false
|
||||
@parsed = false
|
||||
|
||||
if intern
|
||||
@parsed = true
|
||||
parse_init
|
||||
end
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{@body.inspect}>"
|
||||
end
|
||||
|
||||
def illegal?
|
||||
@illegal
|
||||
end
|
||||
|
||||
def empty?
|
||||
ensure_parsed
|
||||
return true if @illegal
|
||||
isempty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_parsed
|
||||
return if @parsed
|
||||
@parsed = true
|
||||
parse
|
||||
end
|
||||
|
||||
# defabstract parse
|
||||
# end
|
||||
|
||||
def clear_parse_status
|
||||
@parsed = false
|
||||
@illegal = false
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def body
|
||||
ensure_parsed
|
||||
v = Decoder.new(s = '')
|
||||
do_accept v
|
||||
v.terminate
|
||||
s
|
||||
end
|
||||
|
||||
def body=( str )
|
||||
@body = str
|
||||
clear_parse_status
|
||||
end
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def accept( strategy )
|
||||
ensure_parsed
|
||||
do_accept strategy
|
||||
strategy.terminate
|
||||
end
|
||||
|
||||
# abstract do_accept
|
||||
|
||||
end
|
||||
|
||||
|
||||
class UnstructuredHeader < HeaderField
|
||||
|
||||
def body
|
||||
ensure_parsed
|
||||
@body
|
||||
end
|
||||
|
||||
def body=( arg )
|
||||
ensure_parsed
|
||||
@body = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_init
|
||||
end
|
||||
|
||||
def parse
|
||||
@body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @body
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.text @body
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class StructuredHeader < HeaderField
|
||||
|
||||
def comments
|
||||
ensure_parsed
|
||||
if @comments[0]
|
||||
[Decoder.decode(@comments[0])]
|
||||
else
|
||||
@comments
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse
|
||||
save = nil
|
||||
|
||||
begin
|
||||
parse_init
|
||||
do_parse
|
||||
rescue SyntaxError
|
||||
if not save and mime_encoded? @body
|
||||
save = @body
|
||||
@body = Decoder.decode(save)
|
||||
retry
|
||||
elsif save
|
||||
@body = save
|
||||
end
|
||||
|
||||
@illegal = true
|
||||
raise if @config.strict_parse?
|
||||
end
|
||||
end
|
||||
|
||||
def parse_init
|
||||
@comments = []
|
||||
init
|
||||
end
|
||||
|
||||
def do_parse
|
||||
quote_boundary
|
||||
obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
|
||||
set obj if obj
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class DateTimeHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :DATETIME
|
||||
|
||||
def date
|
||||
ensure_parsed
|
||||
@date
|
||||
end
|
||||
|
||||
def date=( arg )
|
||||
ensure_parsed
|
||||
@date = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@date = nil
|
||||
end
|
||||
|
||||
def set( t )
|
||||
@date = t
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @date
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta time2str(@date)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class AddressHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :MADDRESS
|
||||
|
||||
def addrs
|
||||
ensure_parsed
|
||||
@addrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@addrs = []
|
||||
end
|
||||
|
||||
def set( a )
|
||||
@addrs = a
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@addrs.empty?
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
first = true
|
||||
@addrs.each do |a|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.meta ','
|
||||
strategy.space
|
||||
end
|
||||
a.accept strategy
|
||||
end
|
||||
|
||||
@comments.each do |c|
|
||||
strategy.space
|
||||
strategy.meta '('
|
||||
strategy.text c
|
||||
strategy.meta ')'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ReturnPathHeader < AddressHeader
|
||||
|
||||
PARSE_TYPE = :RETPATH
|
||||
|
||||
def addr
|
||||
addrs()[0]
|
||||
end
|
||||
|
||||
def spec
|
||||
a = addr() or return nil
|
||||
a.spec
|
||||
end
|
||||
|
||||
def routes
|
||||
a = addr() or return nil
|
||||
a.routes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def do_accept( strategy )
|
||||
a = addr()
|
||||
|
||||
strategy.meta '<'
|
||||
unless a.routes.empty?
|
||||
strategy.meta a.routes.map {|i| '@' + i }.join(',')
|
||||
strategy.meta ':'
|
||||
end
|
||||
spec = a.spec
|
||||
strategy.meta spec if spec
|
||||
strategy.meta '>'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class SingleAddressHeader < AddressHeader
|
||||
|
||||
def addr
|
||||
addrs()[0]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def do_accept( strategy )
|
||||
a = addr()
|
||||
a.accept strategy
|
||||
@comments.each do |c|
|
||||
strategy.space
|
||||
strategy.meta '('
|
||||
strategy.text c
|
||||
strategy.meta ')'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MessageIdHeader < StructuredHeader
|
||||
|
||||
def id
|
||||
ensure_parsed
|
||||
@id
|
||||
end
|
||||
|
||||
def id=( arg )
|
||||
ensure_parsed
|
||||
@id = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@id = nil
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @id
|
||||
end
|
||||
|
||||
def do_parse
|
||||
@id = @body.slice(MESSAGE_ID) or
|
||||
raise SyntaxError, "wrong Message-ID format: #{@body}"
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta @id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ReferencesHeader < StructuredHeader
|
||||
|
||||
def refs
|
||||
ensure_parsed
|
||||
@refs
|
||||
end
|
||||
|
||||
def each_id
|
||||
self.refs.each do |i|
|
||||
yield i if MESSAGE_ID === i
|
||||
end
|
||||
end
|
||||
|
||||
def ids
|
||||
ensure_parsed
|
||||
@ids
|
||||
end
|
||||
|
||||
def each_phrase
|
||||
self.refs.each do |i|
|
||||
yield i unless MESSAGE_ID === i
|
||||
end
|
||||
end
|
||||
|
||||
def phrases
|
||||
ret = []
|
||||
each_phrase {|i| ret.push i }
|
||||
ret
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@refs = []
|
||||
@ids = []
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@ids.empty?
|
||||
end
|
||||
|
||||
def do_parse
|
||||
str = @body
|
||||
while m = MESSAGE_ID.match(str)
|
||||
pre = m.pre_match.strip
|
||||
@refs.push pre unless pre.empty?
|
||||
@refs.push s = m[0]
|
||||
@ids.push s
|
||||
str = m.post_match
|
||||
end
|
||||
str = str.strip
|
||||
@refs.push str unless str.empty?
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
first = true
|
||||
@ids.each do |i|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.space
|
||||
end
|
||||
strategy.meta i
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ReceivedHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :RECEIVED
|
||||
|
||||
def from
|
||||
ensure_parsed
|
||||
@from
|
||||
end
|
||||
|
||||
def from=( arg )
|
||||
ensure_parsed
|
||||
@from = arg
|
||||
end
|
||||
|
||||
def by
|
||||
ensure_parsed
|
||||
@by
|
||||
end
|
||||
|
||||
def by=( arg )
|
||||
ensure_parsed
|
||||
@by = arg
|
||||
end
|
||||
|
||||
def via
|
||||
ensure_parsed
|
||||
@via
|
||||
end
|
||||
|
||||
def via=( arg )
|
||||
ensure_parsed
|
||||
@via = arg
|
||||
end
|
||||
|
||||
def with
|
||||
ensure_parsed
|
||||
@with
|
||||
end
|
||||
|
||||
def id
|
||||
ensure_parsed
|
||||
@id
|
||||
end
|
||||
|
||||
def id=( arg )
|
||||
ensure_parsed
|
||||
@id = arg
|
||||
end
|
||||
|
||||
def _for
|
||||
ensure_parsed
|
||||
@_for
|
||||
end
|
||||
|
||||
def _for=( arg )
|
||||
ensure_parsed
|
||||
@_for = arg
|
||||
end
|
||||
|
||||
def date
|
||||
ensure_parsed
|
||||
@date
|
||||
end
|
||||
|
||||
def date=( arg )
|
||||
ensure_parsed
|
||||
@date = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@from = @by = @via = @with = @id = @_for = nil
|
||||
@with = []
|
||||
@date = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@from, @by, @via, @with, @id, @_for, @date = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@with.empty? and not (@from or @by or @via or @id or @_for or @date)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
list = []
|
||||
list.push 'from ' + @from if @from
|
||||
list.push 'by ' + @by if @by
|
||||
list.push 'via ' + @via if @via
|
||||
@with.each do |i|
|
||||
list.push 'with ' + i
|
||||
end
|
||||
list.push 'id ' + @id if @id
|
||||
list.push 'for <' + @_for + '>' if @_for
|
||||
|
||||
first = true
|
||||
list.each do |i|
|
||||
strategy.space unless first
|
||||
strategy.meta i
|
||||
first = false
|
||||
end
|
||||
if @date
|
||||
strategy.meta ';'
|
||||
strategy.space
|
||||
strategy.meta time2str(@date)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class KeywordsHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :KEYWORDS
|
||||
|
||||
def keys
|
||||
ensure_parsed
|
||||
@keys
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@keys = []
|
||||
end
|
||||
|
||||
def set( a )
|
||||
@keys = a
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@keys.empty?
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
first = true
|
||||
@keys.each do |i|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.meta ','
|
||||
end
|
||||
strategy.meta i
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class EncryptedHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :ENCRYPTED
|
||||
|
||||
def encrypter
|
||||
ensure_parsed
|
||||
@encrypter
|
||||
end
|
||||
|
||||
def encrypter=( arg )
|
||||
ensure_parsed
|
||||
@encrypter = arg
|
||||
end
|
||||
|
||||
def keyword
|
||||
ensure_parsed
|
||||
@keyword
|
||||
end
|
||||
|
||||
def keyword=( arg )
|
||||
ensure_parsed
|
||||
@keyword = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@encrypter = nil
|
||||
@keyword = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@encrypter, @keyword = args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not (@encrypter or @keyword)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
if @key
|
||||
strategy.meta @encrypter + ','
|
||||
strategy.space
|
||||
strategy.meta @keyword
|
||||
else
|
||||
strategy.meta @encrypter
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MimeVersionHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :MIMEVERSION
|
||||
|
||||
def major
|
||||
ensure_parsed
|
||||
@major
|
||||
end
|
||||
|
||||
def major=( arg )
|
||||
ensure_parsed
|
||||
@major = arg
|
||||
end
|
||||
|
||||
def minor
|
||||
ensure_parsed
|
||||
@minor
|
||||
end
|
||||
|
||||
def minor=( arg )
|
||||
ensure_parsed
|
||||
@minor = arg
|
||||
end
|
||||
|
||||
def version
|
||||
sprintf('%d.%d', major, minor)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@major = nil
|
||||
@minor = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@major, @minor = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not (@major or @minor)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta sprintf('%d.%d', @major, @minor)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContentTypeHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :CTYPE
|
||||
|
||||
def main_type
|
||||
ensure_parsed
|
||||
@main
|
||||
end
|
||||
|
||||
def main_type=( arg )
|
||||
ensure_parsed
|
||||
@main = arg.downcase
|
||||
end
|
||||
|
||||
def sub_type
|
||||
ensure_parsed
|
||||
@sub
|
||||
end
|
||||
|
||||
def sub_type=( arg )
|
||||
ensure_parsed
|
||||
@sub = arg.downcase
|
||||
end
|
||||
|
||||
def content_type
|
||||
ensure_parsed
|
||||
@sub ? sprintf('%s/%s', @main, @sub) : @main
|
||||
end
|
||||
|
||||
def params
|
||||
ensure_parsed
|
||||
unless @params.blank?
|
||||
@params.each do |k, v|
|
||||
@params[k] = unquote(v)
|
||||
end
|
||||
end
|
||||
@params
|
||||
end
|
||||
|
||||
def []( key )
|
||||
ensure_parsed
|
||||
@params and unquote(@params[key])
|
||||
end
|
||||
|
||||
def []=( key, val )
|
||||
ensure_parsed
|
||||
(@params ||= {})[key] = val
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@main = @sub = @params = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@main, @sub, @params = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not (@main or @sub)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
if @sub
|
||||
strategy.meta sprintf('%s/%s', @main, @sub)
|
||||
else
|
||||
strategy.meta @main
|
||||
end
|
||||
@params.each do |k,v|
|
||||
if v
|
||||
strategy.meta ';'
|
||||
strategy.space
|
||||
strategy.kv_pair k, v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContentTransferEncodingHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :CENCODING
|
||||
|
||||
def encoding
|
||||
ensure_parsed
|
||||
@encoding
|
||||
end
|
||||
|
||||
def encoding=( arg )
|
||||
ensure_parsed
|
||||
@encoding = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@encoding = nil
|
||||
end
|
||||
|
||||
def set( s )
|
||||
@encoding = s
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @encoding
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta @encoding.capitalize
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContentDispositionHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :CDISPOSITION
|
||||
|
||||
def disposition
|
||||
ensure_parsed
|
||||
@disposition
|
||||
end
|
||||
|
||||
def disposition=( str )
|
||||
ensure_parsed
|
||||
@disposition = str.downcase
|
||||
end
|
||||
|
||||
def params
|
||||
ensure_parsed
|
||||
unless @params.blank?
|
||||
@params.each do |k, v|
|
||||
@params[k] = unquote(v)
|
||||
end
|
||||
end
|
||||
@params
|
||||
end
|
||||
|
||||
def []( key )
|
||||
ensure_parsed
|
||||
@params and unquote(@params[key])
|
||||
end
|
||||
|
||||
def []=( key, val )
|
||||
ensure_parsed
|
||||
(@params ||= {})[key] = val
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@disposition = @params = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@disposition, @params = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @disposition and (not @params or @params.empty?)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta @disposition
|
||||
@params.each do |k,v|
|
||||
strategy.meta ';'
|
||||
strategy.space
|
||||
strategy.kv_pair k, unquote(v)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class HeaderField # redefine
|
||||
|
||||
FNAME_TO_CLASS = {
|
||||
'date' => DateTimeHeader,
|
||||
'resent-date' => DateTimeHeader,
|
||||
'to' => AddressHeader,
|
||||
'cc' => AddressHeader,
|
||||
'bcc' => AddressHeader,
|
||||
'from' => AddressHeader,
|
||||
'reply-to' => AddressHeader,
|
||||
'resent-to' => AddressHeader,
|
||||
'resent-cc' => AddressHeader,
|
||||
'resent-bcc' => AddressHeader,
|
||||
'resent-from' => AddressHeader,
|
||||
'resent-reply-to' => AddressHeader,
|
||||
'sender' => SingleAddressHeader,
|
||||
'resent-sender' => SingleAddressHeader,
|
||||
'return-path' => ReturnPathHeader,
|
||||
'message-id' => MessageIdHeader,
|
||||
'resent-message-id' => MessageIdHeader,
|
||||
'in-reply-to' => ReferencesHeader,
|
||||
'received' => ReceivedHeader,
|
||||
'references' => ReferencesHeader,
|
||||
'keywords' => KeywordsHeader,
|
||||
'encrypted' => EncryptedHeader,
|
||||
'mime-version' => MimeVersionHeader,
|
||||
'content-type' => ContentTypeHeader,
|
||||
'content-transfer-encoding' => ContentTransferEncodingHeader,
|
||||
'content-disposition' => ContentDispositionHeader,
|
||||
'content-id' => MessageIdHeader,
|
||||
'subject' => UnstructuredHeader,
|
||||
'comments' => UnstructuredHeader,
|
||||
'content-description' => UnstructuredHeader
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
||||
@@ -1,9 +0,0 @@
|
||||
#:stopdoc:
|
||||
# This is here for Rolls.
|
||||
# Rolls uses this instead of lib/tmail.rb.
|
||||
|
||||
require 'tmail/version'
|
||||
require 'tmail/mail'
|
||||
require 'tmail/mailbox'
|
||||
require 'tmail/core_extensions'
|
||||
#:startdoc:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
#:stopdoc:
|
||||
require 'tmail/mailbox'
|
||||
#:startdoc:
|
||||
@@ -1,578 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Mail class
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
|
||||
|
||||
require 'tmail/interface'
|
||||
require 'tmail/encode'
|
||||
require 'tmail/header'
|
||||
require 'tmail/port'
|
||||
require 'tmail/config'
|
||||
require 'tmail/utils'
|
||||
require 'tmail/attachments'
|
||||
require 'tmail/quoting'
|
||||
require 'socket'
|
||||
|
||||
module TMail
|
||||
|
||||
# == Mail Class
|
||||
#
|
||||
# Accessing a TMail object done via the TMail::Mail class. As email can be fairly complex
|
||||
# creatures, you will find a large amount of accessor and setter methods in this class!
|
||||
#
|
||||
# Most of the below methods handle the header, in fact, what TMail does best is handle the
|
||||
# header of the email object. There are only a few methods that deal directly with the body
|
||||
# of the email, such as base64_encode and base64_decode.
|
||||
#
|
||||
# === Using TMail inside your code
|
||||
#
|
||||
# The usual way is to install the gem (see the {README}[link:/README] on how to do this) and
|
||||
# then put at the top of your class:
|
||||
#
|
||||
# require 'tmail'
|
||||
#
|
||||
# You can then create a new TMail object in your code with:
|
||||
#
|
||||
# @email = TMail::Mail.new
|
||||
#
|
||||
# Or if you have an email as a string, you can initialize a new TMail::Mail object and get it
|
||||
# to parse that string for you like so:
|
||||
#
|
||||
# @email = TMail::Mail.parse(email_text)
|
||||
#
|
||||
# You can also read a single email off the disk, for example:
|
||||
#
|
||||
# @email = TMail::Mail.load('filename.txt')
|
||||
#
|
||||
# Also, you can read a mailbox (usual unix mbox format) and end up with an array of TMail
|
||||
# objects by doing something like this:
|
||||
#
|
||||
# # Note, we pass true as the last variable to open the mailbox read only
|
||||
# mailbox = TMail::UNIXMbox.new("mailbox", nil, true)
|
||||
# @emails = []
|
||||
# mailbox.each_port { |m| @emails << TMail::Mail.new(m) }
|
||||
#
|
||||
class Mail
|
||||
|
||||
class << self
|
||||
|
||||
# Opens an email that has been saved out as a file by itself.
|
||||
#
|
||||
# This function will read a file non-destructively and then parse
|
||||
# the contents and return a TMail::Mail object.
|
||||
#
|
||||
# Does not handle multiple email mailboxes (like a unix mbox) for that
|
||||
# use the TMail::UNIXMbox class.
|
||||
#
|
||||
# Example:
|
||||
# mail = TMail::Mail.load('filename')
|
||||
#
|
||||
def load( fname )
|
||||
new(FilePort.new(fname))
|
||||
end
|
||||
|
||||
alias load_from load
|
||||
alias loadfrom load
|
||||
|
||||
# Parses an email from the supplied string and returns a TMail::Mail
|
||||
# object.
|
||||
#
|
||||
# Example:
|
||||
# require 'rubygems'; require 'tmail'
|
||||
# email_string =<<HEREDOC
|
||||
# To: mikel@lindsaar.net
|
||||
# From: mikel@me.com
|
||||
# Subject: This is a short Email
|
||||
#
|
||||
# Hello there Mikel!
|
||||
#
|
||||
# HEREDOC
|
||||
# mail = TMail::Mail.parse(email_string)
|
||||
# #=> #<TMail::Mail port=#<TMail::StringPort:id=0xa30ac0> bodyport=nil>
|
||||
# mail.body
|
||||
# #=> "Hello there Mikel!\n\n"
|
||||
def parse( str )
|
||||
new(StringPort.new(str))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def initialize( port = nil, conf = DEFAULT_CONFIG ) #:nodoc:
|
||||
@port = port || StringPort.new
|
||||
@config = Config.to_config(conf)
|
||||
|
||||
@header = {}
|
||||
@body_port = nil
|
||||
@body_parsed = false
|
||||
@epilogue = ''
|
||||
@parts = []
|
||||
|
||||
@port.ropen {|f|
|
||||
parse_header f
|
||||
parse_body f unless @port.reproducible?
|
||||
}
|
||||
end
|
||||
|
||||
# Provides access to the port this email is using to hold it's data
|
||||
#
|
||||
# Example:
|
||||
# mail = TMail::Mail.parse(email_string)
|
||||
# mail.port
|
||||
# #=> #<TMail::StringPort:id=0xa2c952>
|
||||
attr_reader :port
|
||||
|
||||
def inspect
|
||||
"\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
|
||||
end
|
||||
|
||||
#
|
||||
# to_s interfaces
|
||||
#
|
||||
|
||||
public
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def write_back( eol = "\n", charset = 'e' )
|
||||
parse_body
|
||||
@port.wopen {|stream| encoded eol, charset, stream }
|
||||
end
|
||||
|
||||
def accept( strategy )
|
||||
with_multipart_encoding(strategy) {
|
||||
ordered_each do |name, field|
|
||||
next if field.empty?
|
||||
strategy.header_name canonical(name)
|
||||
field.accept strategy
|
||||
strategy.puts
|
||||
end
|
||||
strategy.puts
|
||||
body_port().ropen {|r|
|
||||
strategy.write r.read
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def canonical( name )
|
||||
name.split(/-/).map {|s| s.capitalize }.join('-')
|
||||
end
|
||||
|
||||
def with_multipart_encoding( strategy )
|
||||
if parts().empty? # DO NOT USE @parts
|
||||
yield
|
||||
|
||||
else
|
||||
bound = ::TMail.new_boundary
|
||||
if @header.key? 'content-type'
|
||||
@header['content-type'].params['boundary'] = bound
|
||||
else
|
||||
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
|
||||
end
|
||||
|
||||
yield
|
||||
|
||||
parts().each do |tm|
|
||||
strategy.puts
|
||||
strategy.puts '--' + bound
|
||||
tm.accept strategy
|
||||
end
|
||||
strategy.puts
|
||||
strategy.puts '--' + bound + '--'
|
||||
strategy.write epilogue()
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
### header
|
||||
###
|
||||
|
||||
public
|
||||
|
||||
ALLOW_MULTIPLE = {
|
||||
'received' => true,
|
||||
'resent-date' => true,
|
||||
'resent-from' => true,
|
||||
'resent-sender' => true,
|
||||
'resent-to' => true,
|
||||
'resent-cc' => true,
|
||||
'resent-bcc' => true,
|
||||
'resent-message-id' => true,
|
||||
'comments' => true,
|
||||
'keywords' => true
|
||||
}
|
||||
USE_ARRAY = ALLOW_MULTIPLE
|
||||
|
||||
def header
|
||||
@header.dup
|
||||
end
|
||||
|
||||
# Returns a TMail::AddressHeader object of the field you are querying.
|
||||
# Examples:
|
||||
# @mail['from'] #=> #<TMail::AddressHeader "mikel@test.com.au">
|
||||
# @mail['to'] #=> #<TMail::AddressHeader "mikel@test.com.au">
|
||||
#
|
||||
# You can get the string value of this by passing "to_s" to the query:
|
||||
# Example:
|
||||
# @mail['to'].to_s #=> "mikel@test.com.au"
|
||||
def []( key )
|
||||
@header[key.downcase]
|
||||
end
|
||||
|
||||
def sub_header(key, param)
|
||||
(hdr = self[key]) ? hdr[param] : nil
|
||||
end
|
||||
|
||||
alias fetch []
|
||||
|
||||
# Allows you to set or delete TMail header objects at will.
|
||||
# Examples:
|
||||
# @mail = TMail::Mail.new
|
||||
# @mail['to'].to_s # => 'mikel@test.com.au'
|
||||
# @mail['to'] = 'mikel@elsewhere.org'
|
||||
# @mail['to'].to_s # => 'mikel@elsewhere.org'
|
||||
# @mail.encoded # => "To: mikel@elsewhere.org\r\n\r\n"
|
||||
# @mail['to'] = nil
|
||||
# @mail['to'].to_s # => nil
|
||||
# @mail.encoded # => "\r\n"
|
||||
#
|
||||
# Note: setting mail[] = nil actually deletes the header field in question from the object,
|
||||
# it does not just set the value of the hash to nil
|
||||
def []=( key, val )
|
||||
dkey = key.downcase
|
||||
|
||||
if val.nil?
|
||||
@header.delete dkey
|
||||
return nil
|
||||
end
|
||||
|
||||
case val
|
||||
when String
|
||||
header = new_hf(key, val)
|
||||
when HeaderField
|
||||
;
|
||||
when Array
|
||||
ALLOW_MULTIPLE.include? dkey or
|
||||
raise ArgumentError, "#{key}: Header must not be multiple"
|
||||
@header[dkey] = val
|
||||
return val
|
||||
else
|
||||
header = new_hf(key, val.to_s)
|
||||
end
|
||||
if ALLOW_MULTIPLE.include? dkey
|
||||
(@header[dkey] ||= []).push header
|
||||
else
|
||||
@header[dkey] = header
|
||||
end
|
||||
|
||||
val
|
||||
end
|
||||
|
||||
alias store []=
|
||||
|
||||
# Allows you to loop through each header in the TMail::Mail object in a block
|
||||
# Example:
|
||||
# @mail['to'] = 'mikel@elsewhere.org'
|
||||
# @mail['from'] = 'me@me.com'
|
||||
# @mail.each_header { |k,v| puts "#{k} = #{v}" }
|
||||
# # => from = me@me.com
|
||||
# # => to = mikel@elsewhere.org
|
||||
def each_header
|
||||
@header.each do |key, val|
|
||||
[val].flatten.each {|v| yield key, v }
|
||||
end
|
||||
end
|
||||
|
||||
alias each_pair each_header
|
||||
|
||||
def each_header_name( &block )
|
||||
@header.each_key(&block)
|
||||
end
|
||||
|
||||
alias each_key each_header_name
|
||||
|
||||
def each_field( &block )
|
||||
@header.values.flatten.each(&block)
|
||||
end
|
||||
|
||||
alias each_value each_field
|
||||
|
||||
FIELD_ORDER = %w(
|
||||
return-path received
|
||||
resent-date resent-from resent-sender resent-to
|
||||
resent-cc resent-bcc resent-message-id
|
||||
date from sender reply-to to cc bcc
|
||||
message-id in-reply-to references
|
||||
subject comments keywords
|
||||
mime-version content-type content-transfer-encoding
|
||||
content-disposition content-description
|
||||
)
|
||||
|
||||
def ordered_each
|
||||
list = @header.keys
|
||||
FIELD_ORDER.each do |name|
|
||||
if list.delete(name)
|
||||
[@header[name]].flatten.each {|v| yield name, v }
|
||||
end
|
||||
end
|
||||
list.each do |name|
|
||||
[@header[name]].flatten.each {|v| yield name, v }
|
||||
end
|
||||
end
|
||||
|
||||
def clear
|
||||
@header.clear
|
||||
end
|
||||
|
||||
def delete( key )
|
||||
@header.delete key.downcase
|
||||
end
|
||||
|
||||
def delete_if
|
||||
@header.delete_if do |key,val|
|
||||
if Array === val
|
||||
val.delete_if {|v| yield key, v }
|
||||
val.empty?
|
||||
else
|
||||
yield key, val
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def keys
|
||||
@header.keys
|
||||
end
|
||||
|
||||
def key?( key )
|
||||
@header.key? key.downcase
|
||||
end
|
||||
|
||||
def values_at( *args )
|
||||
args.map {|k| @header[k.downcase] }.flatten
|
||||
end
|
||||
|
||||
alias indexes values_at
|
||||
alias indices values_at
|
||||
|
||||
private
|
||||
|
||||
def parse_header( f )
|
||||
name = field = nil
|
||||
unixfrom = nil
|
||||
|
||||
while line = f.gets
|
||||
case line
|
||||
when /\A[ \t]/ # continue from prev line
|
||||
raise SyntaxError, 'mail is began by space' unless field
|
||||
field << ' ' << line.strip
|
||||
|
||||
when /\A([^\: \t]+):\s*/ # new header line
|
||||
add_hf name, field if field
|
||||
name = $1
|
||||
field = $' #.strip
|
||||
|
||||
when /\A\-*\s*\z/ # end of header
|
||||
add_hf name, field if field
|
||||
name = field = nil
|
||||
break
|
||||
|
||||
when /\AFrom (\S+)/
|
||||
unixfrom = $1
|
||||
|
||||
when /^charset=.*/
|
||||
|
||||
else
|
||||
raise SyntaxError, "wrong mail header: '#{line.inspect}'"
|
||||
end
|
||||
end
|
||||
add_hf name, field if name
|
||||
|
||||
if unixfrom
|
||||
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
|
||||
end
|
||||
end
|
||||
|
||||
def add_hf( name, field )
|
||||
key = name.downcase
|
||||
field = new_hf(name, field)
|
||||
|
||||
if ALLOW_MULTIPLE.include? key
|
||||
(@header[key] ||= []).push field
|
||||
else
|
||||
@header[key] = field
|
||||
end
|
||||
end
|
||||
|
||||
def new_hf( name, field )
|
||||
HeaderField.new(name, field, @config)
|
||||
end
|
||||
|
||||
###
|
||||
### body
|
||||
###
|
||||
|
||||
public
|
||||
|
||||
def body_port
|
||||
parse_body
|
||||
@body_port
|
||||
end
|
||||
|
||||
def each( &block )
|
||||
body_port().ropen {|f| f.each(&block) }
|
||||
end
|
||||
|
||||
def quoted_body
|
||||
body_port.ropen {|f| return f.read }
|
||||
end
|
||||
|
||||
def quoted_body= str
|
||||
body_port.wopen { |f| f.write str }
|
||||
str
|
||||
end
|
||||
|
||||
def body=( str )
|
||||
# Sets the body of the email to a new (encoded) string.
|
||||
#
|
||||
# We also reparses the email if the body is ever reassigned, this is a performance hit, however when
|
||||
# you assign the body, you usually want to be able to make sure that you can access the attachments etc.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# mail.body = "Hello, this is\nthe body text"
|
||||
# # => "Hello, this is\nthe body"
|
||||
# mail.body
|
||||
# # => "Hello, this is\nthe body"
|
||||
@body_parsed = false
|
||||
parse_body(StringInput.new(str))
|
||||
parse_body
|
||||
@body_port.wopen {|f| f.write str }
|
||||
str
|
||||
end
|
||||
|
||||
alias preamble quoted_body
|
||||
alias preamble= quoted_body=
|
||||
|
||||
def epilogue
|
||||
parse_body
|
||||
@epilogue.dup
|
||||
end
|
||||
|
||||
def epilogue=( str )
|
||||
parse_body
|
||||
@epilogue = str
|
||||
str
|
||||
end
|
||||
|
||||
def parts
|
||||
parse_body
|
||||
@parts
|
||||
end
|
||||
|
||||
def each_part( &block )
|
||||
parts().each(&block)
|
||||
end
|
||||
|
||||
# Returns true if the content type of this part of the email is
|
||||
# a disposition attachment
|
||||
def disposition_is_attachment?
|
||||
(self['content-disposition'] && self['content-disposition'].disposition == "attachment")
|
||||
end
|
||||
|
||||
# Returns true if this part's content main type is text, else returns false.
|
||||
# By main type is meant "text/plain" is text. "text/html" is text
|
||||
def content_type_is_text?
|
||||
self.header['content-type'] && (self.header['content-type'].main_type != "text")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_body( f = nil )
|
||||
return if @body_parsed
|
||||
if f
|
||||
parse_body_0 f
|
||||
else
|
||||
@port.ropen {|f|
|
||||
skip_header f
|
||||
parse_body_0 f
|
||||
}
|
||||
end
|
||||
@body_parsed = true
|
||||
end
|
||||
|
||||
def skip_header( f )
|
||||
while line = f.gets
|
||||
return if /\A[\r\n]*\z/ === line
|
||||
end
|
||||
end
|
||||
|
||||
def parse_body_0( f )
|
||||
if multipart?
|
||||
read_multipart f
|
||||
else
|
||||
@body_port = @config.new_body_port(self)
|
||||
@body_port.wopen {|w|
|
||||
w.write f.read
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def read_multipart( src )
|
||||
bound = @header['content-type'].params['boundary']
|
||||
is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
|
||||
lastbound = "--#{bound}--"
|
||||
|
||||
ports = [ @config.new_preamble_port(self) ]
|
||||
begin
|
||||
f = ports.last.wopen
|
||||
while line = src.gets
|
||||
if is_sep === line
|
||||
f.close
|
||||
break if line.strip == lastbound
|
||||
ports.push @config.new_part_port(self)
|
||||
f = ports.last.wopen
|
||||
else
|
||||
f << line
|
||||
end
|
||||
end
|
||||
@epilogue = (src.read || '')
|
||||
ensure
|
||||
f.close if f and not f.closed?
|
||||
end
|
||||
|
||||
@body_port = ports.shift
|
||||
@parts = ports.map {|p| self.class.new(p, @config) }
|
||||
end
|
||||
|
||||
end # class Mail
|
||||
|
||||
end # module TMail
|
||||
@@ -1,495 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Mailbox and Mbox interaction class
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
require 'tmail/port'
|
||||
require 'socket'
|
||||
require 'mutex_m'
|
||||
|
||||
|
||||
unless [].respond_to?(:sort_by)
|
||||
module Enumerable#:nodoc:
|
||||
def sort_by
|
||||
map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class MhMailbox
|
||||
|
||||
PORT_CLASS = MhPort
|
||||
|
||||
def initialize( dir )
|
||||
edir = File.expand_path(dir)
|
||||
raise ArgumentError, "not directory: #{dir}"\
|
||||
unless FileTest.directory? edir
|
||||
@dirname = edir
|
||||
@last_file = nil
|
||||
@last_atime = nil
|
||||
end
|
||||
|
||||
def directory
|
||||
@dirname
|
||||
end
|
||||
|
||||
alias dirname directory
|
||||
|
||||
attr_accessor :last_atime
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{@dirname}>"
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
|
||||
def new_port
|
||||
PORT_CLASS.new(next_file_name())
|
||||
end
|
||||
|
||||
def each_port
|
||||
mail_files().each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
@last_atime = Time.now
|
||||
end
|
||||
|
||||
alias each each_port
|
||||
|
||||
def reverse_each_port
|
||||
mail_files().reverse_each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
@last_atime = Time.now
|
||||
end
|
||||
|
||||
alias reverse_each reverse_each_port
|
||||
|
||||
# old #each_mail returns Port
|
||||
#def each_mail
|
||||
# each_port do |port|
|
||||
# yield Mail.new(port)
|
||||
# end
|
||||
#end
|
||||
|
||||
def each_new_port( mtime = nil, &block )
|
||||
mtime ||= @last_atime
|
||||
return each_port(&block) unless mtime
|
||||
return unless File.mtime(@dirname) >= mtime
|
||||
|
||||
mail_files().each do |path|
|
||||
yield PORT_CLASS.new(path) if File.mtime(path) > mtime
|
||||
end
|
||||
@last_atime = Time.now
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mail_files
|
||||
Dir.entries(@dirname)\
|
||||
.select {|s| /\A\d+\z/ === s }\
|
||||
.map {|s| s.to_i }\
|
||||
.sort\
|
||||
.map {|i| "#{@dirname}/#{i}" }\
|
||||
.select {|path| FileTest.file? path }
|
||||
end
|
||||
|
||||
def next_file_name
|
||||
unless n = @last_file
|
||||
n = 0
|
||||
Dir.entries(@dirname)\
|
||||
.select {|s| /\A\d+\z/ === s }\
|
||||
.map {|s| s.to_i }.sort\
|
||||
.each do |i|
|
||||
next unless FileTest.file? "#{@dirname}/#{i}"
|
||||
n = i
|
||||
end
|
||||
end
|
||||
begin
|
||||
n += 1
|
||||
end while FileTest.exist? "#{@dirname}/#{n}"
|
||||
@last_file = n
|
||||
|
||||
"#{@dirname}/#{n}"
|
||||
end
|
||||
|
||||
end # MhMailbox
|
||||
|
||||
MhLoader = MhMailbox
|
||||
|
||||
|
||||
class UNIXMbox
|
||||
|
||||
class << self
|
||||
alias newobj new
|
||||
end
|
||||
|
||||
# Creates a new mailbox object that you can iterate through to collect the
|
||||
# emails from with "each_port".
|
||||
#
|
||||
# You need to pass it a filename of a unix mailbox format file, the format of this
|
||||
# file can be researched at this page at {wikipedia}[link:http://en.wikipedia.org/wiki/Mbox]
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# +filename+: The filename of the mailbox you want to open
|
||||
#
|
||||
# +tmpdir+: Can be set to override TMail using the system environment's temp dir. TMail will first
|
||||
# use the temp dir specified by you (if any) or then the temp dir specified in the Environment's TEMP
|
||||
# value then the value in the Environment's TMP value or failing all of the above, '/tmp'
|
||||
#
|
||||
# +readonly+: If set to false, each email you take from the mail box will be removed from the mailbox.
|
||||
# default is *false* - ie, it *WILL* truncate your mailbox file to ZERO once it has read the emails out.
|
||||
#
|
||||
# ==== Options:
|
||||
#
|
||||
# None
|
||||
#
|
||||
# ==== Examples:
|
||||
#
|
||||
# # First show using readonly true:
|
||||
#
|
||||
# require 'ftools'
|
||||
# File.size("../test/fixtures/mailbox")
|
||||
# #=> 20426
|
||||
#
|
||||
# mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox", nil, true)
|
||||
# #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=true.....>
|
||||
#
|
||||
# mailbox.each_port do |port|
|
||||
# mail = TMail::Mail.new(port)
|
||||
# puts mail.subject
|
||||
# end
|
||||
# #Testing mailbox 1
|
||||
# #Testing mailbox 2
|
||||
# #Testing mailbox 3
|
||||
# #Testing mailbox 4
|
||||
# require 'ftools'
|
||||
# File.size?("../test/fixtures/mailbox")
|
||||
# #=> 20426
|
||||
#
|
||||
# # Now show with readonly set to the default false
|
||||
#
|
||||
# mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox")
|
||||
# #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=false.....>
|
||||
#
|
||||
# mailbox.each_port do |port|
|
||||
# mail = TMail::Mail.new(port)
|
||||
# puts mail.subject
|
||||
# end
|
||||
# #Testing mailbox 1
|
||||
# #Testing mailbox 2
|
||||
# #Testing mailbox 3
|
||||
# #Testing mailbox 4
|
||||
#
|
||||
# File.size?("../test/fixtures/mailbox")
|
||||
# #=> nil
|
||||
def UNIXMbox.new( filename, tmpdir = nil, readonly = false )
|
||||
tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
|
||||
newobj(filename, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
|
||||
end
|
||||
|
||||
def UNIXMbox.lock( fname )
|
||||
begin
|
||||
f = File.open(fname, 'r+')
|
||||
f.flock File::LOCK_EX
|
||||
yield f
|
||||
ensure
|
||||
f.flock File::LOCK_UN
|
||||
f.close if f and not f.closed?
|
||||
end
|
||||
end
|
||||
|
||||
def UNIXMbox.static_new( fname, dir, readonly = false )
|
||||
newobj(fname, dir, readonly, true)
|
||||
end
|
||||
|
||||
def initialize( fname, mhdir, readonly, static )
|
||||
@filename = fname
|
||||
@readonly = readonly
|
||||
@closed = false
|
||||
|
||||
Dir.mkdir mhdir
|
||||
@real = MhMailbox.new(mhdir)
|
||||
@finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
|
||||
ObjectSpace.define_finalizer self, @finalizer
|
||||
end
|
||||
|
||||
def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
|
||||
lambda {
|
||||
if writeback_p
|
||||
lock(mboxfile) {|f|
|
||||
mh.each_port do |port|
|
||||
f.puts create_from_line(port)
|
||||
port.ropen {|r|
|
||||
f.puts r.read
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
if cleanup_p
|
||||
Dir.foreach(mh.dirname) do |fname|
|
||||
next if /\A\.\.?\z/ === fname
|
||||
File.unlink "#{mh.dirname}/#{fname}"
|
||||
end
|
||||
Dir.rmdir mh.dirname
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# make _From line
|
||||
def UNIXMbox.create_from_line( port )
|
||||
sprintf 'From %s %s',
|
||||
fromaddr(), TextUtils.time2str(File.mtime(port.filename))
|
||||
end
|
||||
|
||||
def UNIXMbox.fromaddr(port)
|
||||
h = HeaderField.new_from_port(port, 'Return-Path') ||
|
||||
HeaderField.new_from_port(port, 'From') ||
|
||||
HeaderField.new_from_port(port, 'EnvelopeSender') or return 'nobody'
|
||||
a = h.addrs[0] or return 'nobody'
|
||||
a.spec
|
||||
end
|
||||
|
||||
def close
|
||||
return if @closed
|
||||
|
||||
ObjectSpace.undefine_finalizer self
|
||||
@finalizer.call
|
||||
@finalizer = nil
|
||||
@real = nil
|
||||
@closed = true
|
||||
@updated = nil
|
||||
end
|
||||
|
||||
def each_port( &block )
|
||||
close_check
|
||||
update
|
||||
@real.each_port(&block)
|
||||
end
|
||||
|
||||
alias each each_port
|
||||
|
||||
def reverse_each_port( &block )
|
||||
close_check
|
||||
update
|
||||
@real.reverse_each_port(&block)
|
||||
end
|
||||
|
||||
alias reverse_each reverse_each_port
|
||||
|
||||
# old #each_mail returns Port
|
||||
#def each_mail( &block )
|
||||
# each_port do |port|
|
||||
# yield Mail.new(port)
|
||||
# end
|
||||
#end
|
||||
|
||||
def each_new_port( mtime = nil )
|
||||
close_check
|
||||
update
|
||||
@real.each_new_port(mtime) {|p| yield p }
|
||||
end
|
||||
|
||||
def new_port
|
||||
close_check
|
||||
@real.new_port
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def close_check
|
||||
@closed and raise ArgumentError, 'accessing already closed mbox'
|
||||
end
|
||||
|
||||
def update
|
||||
return if FileTest.zero?(@filename)
|
||||
return if @updated and File.mtime(@filename) < @updated
|
||||
w = nil
|
||||
port = nil
|
||||
time = nil
|
||||
UNIXMbox.lock(@filename) {|f|
|
||||
begin
|
||||
f.each do |line|
|
||||
if /\AFrom / === line
|
||||
w.close if w
|
||||
File.utime time, time, port.filename if time
|
||||
|
||||
port = @real.new_port
|
||||
w = port.wopen
|
||||
time = fromline2time(line)
|
||||
else
|
||||
w.print line if w
|
||||
end
|
||||
end
|
||||
ensure
|
||||
if w and not w.closed?
|
||||
w.close
|
||||
File.utime time, time, port.filename if time
|
||||
end
|
||||
end
|
||||
f.truncate(0) unless @readonly
|
||||
@updated = Time.now
|
||||
}
|
||||
end
|
||||
|
||||
def fromline2time( line )
|
||||
m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \
|
||||
or return nil
|
||||
Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
|
||||
end
|
||||
|
||||
end # UNIXMbox
|
||||
|
||||
MboxLoader = UNIXMbox
|
||||
|
||||
|
||||
class Maildir
|
||||
|
||||
extend Mutex_m
|
||||
|
||||
PORT_CLASS = MaildirPort
|
||||
|
||||
@seq = 0
|
||||
def Maildir.unique_number
|
||||
synchronize {
|
||||
@seq += 1
|
||||
return @seq
|
||||
}
|
||||
end
|
||||
|
||||
def initialize( dir = nil )
|
||||
@dirname = dir || ENV['MAILDIR']
|
||||
raise ArgumentError, "not directory: #{@dirname}"\
|
||||
unless FileTest.directory? @dirname
|
||||
@new = "#{@dirname}/new"
|
||||
@tmp = "#{@dirname}/tmp"
|
||||
@cur = "#{@dirname}/cur"
|
||||
end
|
||||
|
||||
def directory
|
||||
@dirname
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{@dirname}>"
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
|
||||
def each_port
|
||||
mail_files(@cur).each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
end
|
||||
|
||||
alias each each_port
|
||||
|
||||
def reverse_each_port
|
||||
mail_files(@cur).reverse_each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
end
|
||||
|
||||
alias reverse_each reverse_each_port
|
||||
|
||||
def new_port
|
||||
fname = nil
|
||||
tmpfname = nil
|
||||
newfname = nil
|
||||
|
||||
begin
|
||||
fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
|
||||
|
||||
tmpfname = "#{@tmp}/#{fname}"
|
||||
newfname = "#{@new}/#{fname}"
|
||||
end while FileTest.exist? tmpfname
|
||||
|
||||
if block_given?
|
||||
File.open(tmpfname, 'w') {|f| yield f }
|
||||
File.rename tmpfname, newfname
|
||||
PORT_CLASS.new(newfname)
|
||||
else
|
||||
File.open(tmpfname, 'w') {|f| f.write "\n\n" }
|
||||
PORT_CLASS.new(tmpfname)
|
||||
end
|
||||
end
|
||||
|
||||
def each_new_port
|
||||
mail_files(@new).each do |path|
|
||||
dest = @cur + '/' + File.basename(path)
|
||||
File.rename path, dest
|
||||
yield PORT_CLASS.new(dest)
|
||||
end
|
||||
|
||||
check_tmp
|
||||
end
|
||||
|
||||
TOO_OLD = 60 * 60 * 36 # 36 hour
|
||||
|
||||
def check_tmp
|
||||
old = Time.now.to_i - TOO_OLD
|
||||
|
||||
each_filename(@tmp) do |full, fname|
|
||||
if FileTest.file? full and
|
||||
File.stat(full).mtime.to_i < old
|
||||
File.unlink full
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mail_files( dir )
|
||||
Dir.entries(dir)\
|
||||
.select {|s| s[0] != ?. }\
|
||||
.sort_by {|s| s.slice(/\A\d+/).to_i }\
|
||||
.map {|s| "#{dir}/#{s}" }\
|
||||
.select {|path| FileTest.file? path }
|
||||
end
|
||||
|
||||
def each_filename( dir )
|
||||
Dir.foreach(dir) do |fname|
|
||||
path = "#{dir}/#{fname}"
|
||||
if fname[0] != ?. and FileTest.file? path
|
||||
yield path, fname
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # Maildir
|
||||
|
||||
MaildirLoader = Maildir
|
||||
|
||||
end # module TMail
|
||||
@@ -1,6 +0,0 @@
|
||||
#:stopdoc:
|
||||
require 'tmail/version'
|
||||
require 'tmail/mail'
|
||||
require 'tmail/mailbox'
|
||||
require 'tmail/core_extensions'
|
||||
#:startdoc:
|
||||
@@ -1,3 +0,0 @@
|
||||
#:stopdoc:
|
||||
require 'tmail/mailbox'
|
||||
#:startdoc:
|
||||
@@ -1,248 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
#:stopdoc:
|
||||
require 'nkf'
|
||||
#:startdoc:
|
||||
|
||||
module TMail
|
||||
|
||||
class Mail
|
||||
|
||||
def send_to( smtp )
|
||||
do_send_to(smtp) do
|
||||
ready_to_send
|
||||
end
|
||||
end
|
||||
|
||||
def send_text_to( smtp )
|
||||
do_send_to(smtp) do
|
||||
ready_to_send
|
||||
mime_encode
|
||||
end
|
||||
end
|
||||
|
||||
def do_send_to( smtp )
|
||||
from = from_address or raise ArgumentError, 'no from address'
|
||||
(dests = destinations).empty? and raise ArgumentError, 'no receipient'
|
||||
yield
|
||||
send_to_0 smtp, from, dests
|
||||
end
|
||||
private :do_send_to
|
||||
|
||||
def send_to_0( smtp, from, to )
|
||||
smtp.ready(from, to) do |f|
|
||||
encoded "\r\n", 'j', f, ''
|
||||
end
|
||||
end
|
||||
|
||||
def ready_to_send
|
||||
delete_no_send_fields
|
||||
add_message_id
|
||||
add_date
|
||||
end
|
||||
|
||||
NOSEND_FIELDS = %w(
|
||||
received
|
||||
bcc
|
||||
)
|
||||
|
||||
def delete_no_send_fields
|
||||
NOSEND_FIELDS.each do |nm|
|
||||
delete nm
|
||||
end
|
||||
delete_if {|n,v| v.empty? }
|
||||
end
|
||||
|
||||
def add_message_id( fqdn = nil )
|
||||
self.message_id = ::TMail::new_message_id(fqdn)
|
||||
end
|
||||
|
||||
def add_date
|
||||
self.date = Time.now
|
||||
end
|
||||
|
||||
def mime_encode
|
||||
if parts.empty?
|
||||
mime_encode_singlepart
|
||||
else
|
||||
mime_encode_multipart true
|
||||
end
|
||||
end
|
||||
|
||||
def mime_encode_singlepart
|
||||
self.mime_version = '1.0'
|
||||
b = body
|
||||
if NKF.guess(b) != NKF::BINARY
|
||||
mime_encode_text b
|
||||
else
|
||||
mime_encode_binary b
|
||||
end
|
||||
end
|
||||
|
||||
def mime_encode_text( body )
|
||||
self.body = NKF.nkf('-j -m0', body)
|
||||
self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
|
||||
self.encoding = '7bit'
|
||||
end
|
||||
|
||||
def mime_encode_binary( body )
|
||||
self.body = [body].pack('m')
|
||||
self.set_content_type 'application', 'octet-stream'
|
||||
self.encoding = 'Base64'
|
||||
end
|
||||
|
||||
def mime_encode_multipart( top = true )
|
||||
self.mime_version = '1.0' if top
|
||||
self.set_content_type 'multipart', 'mixed'
|
||||
e = encoding(nil)
|
||||
if e and not /\A(?:7bit|8bit|binary)\z/i === e
|
||||
raise ArgumentError,
|
||||
'using C.T.Encoding with multipart mail is not permitted'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#:stopdoc:
|
||||
class DeleteFields
|
||||
|
||||
NOSEND_FIELDS = %w(
|
||||
received
|
||||
bcc
|
||||
)
|
||||
|
||||
def initialize( nosend = nil, delempty = true )
|
||||
@no_send_fields = nosend || NOSEND_FIELDS.dup
|
||||
@delete_empty_fields = delempty
|
||||
end
|
||||
|
||||
attr :no_send_fields
|
||||
attr :delete_empty_fields, true
|
||||
|
||||
def exec( mail )
|
||||
@no_send_fields.each do |nm|
|
||||
delete nm
|
||||
end
|
||||
delete_if {|n,v| v.empty? } if @delete_empty_fields
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
|
||||
#:stopdoc:
|
||||
class AddMessageId
|
||||
|
||||
def initialize( fqdn = nil )
|
||||
@fqdn = fqdn
|
||||
end
|
||||
|
||||
attr :fqdn, true
|
||||
|
||||
def exec( mail )
|
||||
mail.message_id = ::TMail::new_msgid(@fqdn)
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
|
||||
#:stopdoc:
|
||||
class AddDate
|
||||
|
||||
def exec( mail )
|
||||
mail.date = Time.now
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
|
||||
#:stopdoc:
|
||||
class MimeEncodeAuto
|
||||
|
||||
def initialize( s = nil, m = nil )
|
||||
@singlepart_composer = s || MimeEncodeSingle.new
|
||||
@multipart_composer = m || MimeEncodeMulti.new
|
||||
end
|
||||
|
||||
attr :singlepart_composer
|
||||
attr :multipart_composer
|
||||
|
||||
def exec( mail )
|
||||
if mail._builtin_multipart?
|
||||
then @multipart_composer
|
||||
else @singlepart_composer end.exec mail
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
|
||||
#:stopdoc:
|
||||
class MimeEncodeSingle
|
||||
|
||||
def exec( mail )
|
||||
mail.mime_version = '1.0'
|
||||
b = mail.body
|
||||
if NKF.guess(b) != NKF::BINARY
|
||||
on_text b
|
||||
else
|
||||
on_binary b
|
||||
end
|
||||
end
|
||||
|
||||
def on_text( body )
|
||||
mail.body = NKF.nkf('-j -m0', body)
|
||||
mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
|
||||
mail.encoding = '7bit'
|
||||
end
|
||||
|
||||
def on_binary( body )
|
||||
mail.body = [body].pack('m')
|
||||
mail.set_content_type 'application', 'octet-stream'
|
||||
mail.encoding = 'Base64'
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
|
||||
#:stopdoc:
|
||||
class MimeEncodeMulti
|
||||
|
||||
def exec( mail, top = true )
|
||||
mail.mime_version = '1.0' if top
|
||||
mail.set_content_type 'multipart', 'mixed'
|
||||
e = encoding(nil)
|
||||
if e and not /\A(?:7bit|8bit|binary)\z/i === e
|
||||
raise ArgumentError,
|
||||
'using C.T.Encoding with multipart mail is not permitted'
|
||||
end
|
||||
mail.parts.each do |m|
|
||||
exec m, false if m._builtin_multipart?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
#:startdoc:
|
||||
end # module TMail
|
||||
@@ -1,132 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Obsolete methods that are depriciated
|
||||
|
||||
If you really want to see them, go to lib/tmail/obsolete.rb and view to your
|
||||
heart's content.
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
#:stopdoc:
|
||||
module TMail #:nodoc:
|
||||
|
||||
class Mail
|
||||
alias include? key?
|
||||
alias has_key? key?
|
||||
|
||||
def values
|
||||
ret = []
|
||||
each_field {|v| ret.push v }
|
||||
ret
|
||||
end
|
||||
|
||||
def value?( val )
|
||||
HeaderField === val or return false
|
||||
|
||||
[ @header[val.name.downcase] ].flatten.include? val
|
||||
end
|
||||
|
||||
alias has_value? value?
|
||||
end
|
||||
|
||||
class Mail
|
||||
def from_addr( default = nil )
|
||||
addr, = from_addrs(nil)
|
||||
addr || default
|
||||
end
|
||||
|
||||
def from_address( default = nil )
|
||||
if a = from_addr(nil)
|
||||
a.spec
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
alias from_address= from_addrs=
|
||||
|
||||
def from_phrase( default = nil )
|
||||
if a = from_addr(nil)
|
||||
a.phrase
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
alias msgid message_id
|
||||
alias msgid= message_id=
|
||||
|
||||
alias each_dest each_destination
|
||||
end
|
||||
|
||||
class Address
|
||||
alias route routes
|
||||
alias addr spec
|
||||
|
||||
def spec=( str )
|
||||
@local, @domain = str.split(/@/,2).map {|s| s.split(/\./) }
|
||||
end
|
||||
|
||||
alias addr= spec=
|
||||
alias address= spec=
|
||||
end
|
||||
|
||||
class MhMailbox
|
||||
alias new_mail new_port
|
||||
alias each_mail each_port
|
||||
alias each_newmail each_new_port
|
||||
end
|
||||
class UNIXMbox
|
||||
alias new_mail new_port
|
||||
alias each_mail each_port
|
||||
alias each_newmail each_new_port
|
||||
end
|
||||
class Maildir
|
||||
alias new_mail new_port
|
||||
alias each_mail each_port
|
||||
alias each_newmail each_new_port
|
||||
end
|
||||
|
||||
extend TextUtils
|
||||
|
||||
class << self
|
||||
alias msgid? message_id?
|
||||
alias boundary new_boundary
|
||||
alias msgid new_message_id
|
||||
alias new_msgid new_message_id
|
||||
end
|
||||
|
||||
def Mail.boundary
|
||||
::TMail.new_boundary
|
||||
end
|
||||
|
||||
def Mail.msgid
|
||||
::TMail.new_message_id
|
||||
end
|
||||
|
||||
end # module TMail
|
||||
#:startdoc:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,379 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Port class
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
require 'tmail/stringio'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Port
|
||||
def reproducible?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### FilePort
|
||||
###
|
||||
|
||||
class FilePort < Port
|
||||
|
||||
def initialize( fname )
|
||||
@filename = File.expand_path(fname)
|
||||
super()
|
||||
end
|
||||
|
||||
attr_reader :filename
|
||||
|
||||
alias ident filename
|
||||
|
||||
def ==( other )
|
||||
other.respond_to?(:filename) and @filename == other.filename
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
@filename.hash
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:#{@filename}>"
|
||||
end
|
||||
|
||||
def reproducible?
|
||||
true
|
||||
end
|
||||
|
||||
def size
|
||||
File.size @filename
|
||||
end
|
||||
|
||||
|
||||
def ropen( &block )
|
||||
File.open(@filename, &block)
|
||||
end
|
||||
|
||||
def wopen( &block )
|
||||
File.open(@filename, 'w', &block)
|
||||
end
|
||||
|
||||
def aopen( &block )
|
||||
File.open(@filename, 'a', &block)
|
||||
end
|
||||
|
||||
|
||||
def read_all
|
||||
ropen {|f|
|
||||
return f.read
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
def remove
|
||||
File.unlink @filename
|
||||
end
|
||||
|
||||
def move_to( port )
|
||||
begin
|
||||
File.link @filename, port.filename
|
||||
rescue Errno::EXDEV
|
||||
copy_to port
|
||||
end
|
||||
File.unlink @filename
|
||||
end
|
||||
|
||||
alias mv move_to
|
||||
|
||||
def copy_to( port )
|
||||
if FilePort === port
|
||||
copy_file @filename, port.filename
|
||||
else
|
||||
File.open(@filename) {|r|
|
||||
port.wopen {|w|
|
||||
while s = r.sysread(4096)
|
||||
w.write << s
|
||||
end
|
||||
} }
|
||||
end
|
||||
end
|
||||
|
||||
alias cp copy_to
|
||||
|
||||
private
|
||||
|
||||
# from fileutils.rb
|
||||
def copy_file( src, dest )
|
||||
st = r = w = nil
|
||||
|
||||
File.open(src, 'rb') {|r|
|
||||
File.open(dest, 'wb') {|w|
|
||||
st = r.stat
|
||||
begin
|
||||
while true
|
||||
w.write r.sysread(st.blksize)
|
||||
end
|
||||
rescue EOFError
|
||||
end
|
||||
} }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
module MailFlags
|
||||
|
||||
def seen=( b )
|
||||
set_status 'S', b
|
||||
end
|
||||
|
||||
def seen?
|
||||
get_status 'S'
|
||||
end
|
||||
|
||||
def replied=( b )
|
||||
set_status 'R', b
|
||||
end
|
||||
|
||||
def replied?
|
||||
get_status 'R'
|
||||
end
|
||||
|
||||
def flagged=( b )
|
||||
set_status 'F', b
|
||||
end
|
||||
|
||||
def flagged?
|
||||
get_status 'F'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def procinfostr( str, tag, true_p )
|
||||
a = str.upcase.split(//)
|
||||
a.push true_p ? tag : nil
|
||||
a.delete tag unless true_p
|
||||
a.compact.sort.join('').squeeze
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MhPort < FilePort
|
||||
|
||||
include MailFlags
|
||||
|
||||
private
|
||||
|
||||
def set_status( tag, flag )
|
||||
begin
|
||||
tmpfile = @filename + '.tmailtmp.' + $$.to_s
|
||||
File.open(tmpfile, 'w') {|f|
|
||||
write_status f, tag, flag
|
||||
}
|
||||
File.unlink @filename
|
||||
File.link tmpfile, @filename
|
||||
ensure
|
||||
File.unlink tmpfile
|
||||
end
|
||||
end
|
||||
|
||||
def write_status( f, tag, flag )
|
||||
stat = ''
|
||||
File.open(@filename) {|r|
|
||||
while line = r.gets
|
||||
if line.strip.empty?
|
||||
break
|
||||
elsif m = /\AX-TMail-Status:/i.match(line)
|
||||
stat = m.post_match.strip
|
||||
else
|
||||
f.print line
|
||||
end
|
||||
end
|
||||
|
||||
s = procinfostr(stat, tag, flag)
|
||||
f.puts 'X-TMail-Status: ' + s unless s.empty?
|
||||
f.puts
|
||||
|
||||
while s = r.read(2048)
|
||||
f.write s
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def get_status( tag )
|
||||
File.foreach(@filename) {|line|
|
||||
return false if line.strip.empty?
|
||||
if m = /\AX-TMail-Status:/i.match(line)
|
||||
return m.post_match.strip.include?(tag[0])
|
||||
end
|
||||
}
|
||||
false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MaildirPort < FilePort
|
||||
|
||||
def move_to_new
|
||||
new = replace_dir(@filename, 'new')
|
||||
File.rename @filename, new
|
||||
@filename = new
|
||||
end
|
||||
|
||||
def move_to_cur
|
||||
new = replace_dir(@filename, 'cur')
|
||||
File.rename @filename, new
|
||||
@filename = new
|
||||
end
|
||||
|
||||
def replace_dir( path, dir )
|
||||
"#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
|
||||
end
|
||||
private :replace_dir
|
||||
|
||||
|
||||
include MailFlags
|
||||
|
||||
private
|
||||
|
||||
MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
|
||||
|
||||
def set_status( tag, flag )
|
||||
if m = MAIL_FILE.match(File.basename(@filename))
|
||||
s, uniq, type, info, = m.to_a
|
||||
return if type and type != '2' # do not change anything
|
||||
newname = File.dirname(@filename) + '/' +
|
||||
uniq + ':2,' + procinfostr(info.to_s, tag, flag)
|
||||
else
|
||||
newname = @filename + ':2,' + tag
|
||||
end
|
||||
|
||||
File.link @filename, newname
|
||||
File.unlink @filename
|
||||
@filename = newname
|
||||
end
|
||||
|
||||
def get_status( tag )
|
||||
m = MAIL_FILE.match(File.basename(@filename)) or return false
|
||||
m[2] == '2' and m[3].to_s.include?(tag[0])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### StringPort
|
||||
###
|
||||
|
||||
class StringPort < Port
|
||||
|
||||
def initialize( str = '' )
|
||||
@buffer = str
|
||||
super()
|
||||
end
|
||||
|
||||
def string
|
||||
@buffer
|
||||
end
|
||||
|
||||
def to_s
|
||||
@buffer.dup
|
||||
end
|
||||
|
||||
alias read_all to_s
|
||||
|
||||
def size
|
||||
@buffer.size
|
||||
end
|
||||
|
||||
def ==( other )
|
||||
StringPort === other and @buffer.equal? other.string
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
@buffer.object_id.hash
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_id}>"
|
||||
end
|
||||
|
||||
def reproducible?
|
||||
true
|
||||
end
|
||||
|
||||
def ropen( &block )
|
||||
@buffer or raise Errno::ENOENT, "#{inspect} is already removed"
|
||||
StringInput.open(@buffer, &block)
|
||||
end
|
||||
|
||||
def wopen( &block )
|
||||
@buffer = ''
|
||||
StringOutput.new(@buffer, &block)
|
||||
end
|
||||
|
||||
def aopen( &block )
|
||||
@buffer ||= ''
|
||||
StringOutput.new(@buffer, &block)
|
||||
end
|
||||
|
||||
def remove
|
||||
@buffer = nil
|
||||
end
|
||||
|
||||
alias rm remove
|
||||
|
||||
def copy_to( port )
|
||||
port.wopen {|f|
|
||||
f.write @buffer
|
||||
}
|
||||
end
|
||||
|
||||
alias cp copy_to
|
||||
|
||||
def move_to( port )
|
||||
if StringPort === port
|
||||
str = @buffer
|
||||
port.instance_eval { @buffer = str }
|
||||
else
|
||||
copy_to port
|
||||
end
|
||||
remove
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
||||
@@ -1,118 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Quoting methods
|
||||
|
||||
=end
|
||||
module TMail
|
||||
class Mail
|
||||
def subject(to_charset = 'utf-8')
|
||||
Unquoter.unquote_and_convert_to(quoted_subject, to_charset)
|
||||
end
|
||||
|
||||
def unquoted_body(to_charset = 'utf-8')
|
||||
from_charset = sub_header("content-type", "charset")
|
||||
case (content_transfer_encoding || "7bit").downcase
|
||||
when "quoted-printable"
|
||||
# the default charset is set to iso-8859-1 instead of 'us-ascii'.
|
||||
# This is needed as many mailer do not set the charset but send in ISO. This is only used if no charset is set.
|
||||
if !from_charset.blank? && from_charset.downcase == 'us-ascii'
|
||||
from_charset = 'iso-8859-1'
|
||||
end
|
||||
|
||||
Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
|
||||
to_charset, from_charset, true)
|
||||
when "base64"
|
||||
Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
|
||||
from_charset)
|
||||
when "7bit", "8bit"
|
||||
Unquoter.convert_to(quoted_body, to_charset, from_charset)
|
||||
when "binary"
|
||||
quoted_body
|
||||
else
|
||||
quoted_body
|
||||
end
|
||||
end
|
||||
|
||||
def body(to_charset = 'utf-8', &block)
|
||||
attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" }
|
||||
|
||||
if multipart?
|
||||
parts.collect { |part|
|
||||
header = part["content-type"]
|
||||
|
||||
if part.multipart?
|
||||
part.body(to_charset, &attachment_presenter)
|
||||
elsif header.nil?
|
||||
""
|
||||
elsif !attachment?(part)
|
||||
part.unquoted_body(to_charset)
|
||||
else
|
||||
attachment_presenter.call(header["name"] || "(unnamed)")
|
||||
end
|
||||
}.join
|
||||
else
|
||||
unquoted_body(to_charset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Unquoter
|
||||
class << self
|
||||
def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false)
|
||||
return "" if text.nil?
|
||||
text.gsub(/(.*?)(?:(?:=\?(.*?)\?(.)\?(.*?)\?=)|$)/) do
|
||||
before = $1
|
||||
from_charset = $2
|
||||
quoting_method = $3
|
||||
text = $4
|
||||
|
||||
before = convert_to(before, to_charset, from_charset) if before.length > 0
|
||||
before + case quoting_method
|
||||
when "q", "Q" then
|
||||
unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores)
|
||||
when "b", "B" then
|
||||
unquote_base64_and_convert_to(text, to_charset, from_charset)
|
||||
when nil then
|
||||
# will be nil at the end of the string, due to the nature of
|
||||
# the regex used.
|
||||
""
|
||||
else
|
||||
raise "unknown quoting method #{quoting_method.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false)
|
||||
text = text.gsub(/_/, " ") unless preserve_underscores
|
||||
text = text.gsub(/\r\n|\r/, "\n") # normalize newlines
|
||||
convert_to(text.unpack("M*").first, to, from)
|
||||
end
|
||||
|
||||
def unquote_base64_and_convert_to(text, to, from)
|
||||
convert_to(Base64.decode(text), to, from)
|
||||
end
|
||||
|
||||
begin
|
||||
require 'iconv'
|
||||
def convert_to(text, to, from)
|
||||
return text unless to && from
|
||||
text ? Iconv.iconv(to, from, text).first : ""
|
||||
rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
|
||||
# the 'from' parameter specifies a charset other than what the text
|
||||
# actually is...not much we can do in this case but just return the
|
||||
# unconverted text.
|
||||
#
|
||||
# Ditto if either parameter represents an unknown charset, like
|
||||
# X-UNKNOWN.
|
||||
text
|
||||
end
|
||||
rescue LoadError
|
||||
# Not providing quoting support
|
||||
def convert_to(text, to, from)
|
||||
warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})"
|
||||
text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,58 +0,0 @@
|
||||
#:stopdoc:
|
||||
require 'rbconfig'
|
||||
|
||||
# Attempts to require anative extension.
|
||||
# Falls back to pure-ruby version, if it fails.
|
||||
#
|
||||
# This uses Config::CONFIG['arch'] from rbconfig.
|
||||
|
||||
def require_arch(fname)
|
||||
arch = Config::CONFIG['arch']
|
||||
begin
|
||||
path = File.join("tmail", arch, fname)
|
||||
require path
|
||||
rescue LoadError => e
|
||||
# try pre-built Windows binaries
|
||||
if arch =~ /mswin/
|
||||
require File.join("tmail", 'mswin32', fname)
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# def require_arch(fname)
|
||||
# dext = Config::CONFIG['DLEXT']
|
||||
# begin
|
||||
# if File.extname(fname) == dext
|
||||
# path = fname
|
||||
# else
|
||||
# path = File.join("tmail","#{fname}.#{dext}")
|
||||
# end
|
||||
# require path
|
||||
# rescue LoadError => e
|
||||
# begin
|
||||
# arch = Config::CONFIG['arch']
|
||||
# path = File.join("tmail", arch, "#{fname}.#{dext}")
|
||||
# require path
|
||||
# rescue LoadError
|
||||
# case path
|
||||
# when /i686/
|
||||
# path.sub!('i686', 'i586')
|
||||
# when /i586/
|
||||
# path.sub!('i586', 'i486')
|
||||
# when /i486/
|
||||
# path.sub!('i486', 'i386')
|
||||
# else
|
||||
# begin
|
||||
# require fname + '.rb'
|
||||
# rescue LoadError
|
||||
# raise e
|
||||
# end
|
||||
# end
|
||||
# retry
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#:startdoc:
|
||||
@@ -1,49 +0,0 @@
|
||||
=begin rdoc
|
||||
|
||||
= Scanner for TMail
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
#:stopdoc:
|
||||
#require 'tmail/require_arch'
|
||||
require 'tmail/utils'
|
||||
require 'tmail/config'
|
||||
|
||||
module TMail
|
||||
# NOTE: It woiuld be nice if these two libs could boith be called "tmailscanner", and
|
||||
# the native extension would have precedence. However RubyGems boffs that up b/c
|
||||
# it does not gaurantee load_path order.
|
||||
begin
|
||||
raise LoadError, 'Turned off native extentions by user choice' if ENV['NORUBYEXT']
|
||||
require('tmail/tmailscanner') # c extension
|
||||
Scanner = TMailScanner
|
||||
rescue LoadError
|
||||
require 'tmail/scanner_r'
|
||||
Scanner = TMailScanner
|
||||
end
|
||||
end
|
||||
#:stopdoc:
|
||||
@@ -1,261 +0,0 @@
|
||||
# scanner_r.rb
|
||||
#
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
#:stopdoc:
|
||||
require 'tmail/config'
|
||||
|
||||
module TMail
|
||||
|
||||
class TMailScanner
|
||||
|
||||
Version = '1.2.3'
|
||||
Version.freeze
|
||||
|
||||
MIME_HEADERS = {
|
||||
:CTYPE => true,
|
||||
:CENCODING => true,
|
||||
:CDISPOSITION => true
|
||||
}
|
||||
|
||||
alnum = 'a-zA-Z0-9'
|
||||
atomsyms = %q[ _#!$%&`'*+-{|}~^/=? ].strip
|
||||
tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip
|
||||
atomchars = alnum + Regexp.quote(atomsyms)
|
||||
tokenchars = alnum + Regexp.quote(tokensyms)
|
||||
iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
|
||||
|
||||
eucstr = "(?:[\xa1-\xfe][\xa1-\xfe])+"
|
||||
sjisstr = "(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+"
|
||||
utf8str = "(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+"
|
||||
|
||||
quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
|
||||
domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
|
||||
comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
|
||||
|
||||
quoted_without_iso2022 = /\A[^\\"]+/n
|
||||
domlit_without_iso2022 = /\A[^\\\]]+/n
|
||||
comment_without_iso2022 = /\A[^\\()]+/n
|
||||
|
||||
PATTERN_TABLE = {}
|
||||
PATTERN_TABLE['EUC'] =
|
||||
[
|
||||
/\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
|
||||
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
|
||||
quoted_with_iso2022,
|
||||
domlit_with_iso2022,
|
||||
comment_with_iso2022
|
||||
]
|
||||
PATTERN_TABLE['SJIS'] =
|
||||
[
|
||||
/\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
|
||||
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
|
||||
quoted_with_iso2022,
|
||||
domlit_with_iso2022,
|
||||
comment_with_iso2022
|
||||
]
|
||||
PATTERN_TABLE['UTF8'] =
|
||||
[
|
||||
/\A(?:[#{atomchars}]+|#{utf8str})+/n,
|
||||
/\A(?:[#{tokenchars}]+|#{utf8str})+/n,
|
||||
quoted_without_iso2022,
|
||||
domlit_without_iso2022,
|
||||
comment_without_iso2022
|
||||
]
|
||||
PATTERN_TABLE['NONE'] =
|
||||
[
|
||||
/\A[#{atomchars}]+/n,
|
||||
/\A[#{tokenchars}]+/n,
|
||||
quoted_without_iso2022,
|
||||
domlit_without_iso2022,
|
||||
comment_without_iso2022
|
||||
]
|
||||
|
||||
|
||||
def initialize( str, scantype, comments )
|
||||
init_scanner str
|
||||
@comments = comments || []
|
||||
@debug = false
|
||||
|
||||
# fix scanner mode
|
||||
@received = (scantype == :RECEIVED)
|
||||
@is_mime_header = MIME_HEADERS[scantype]
|
||||
|
||||
atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[TMail.KCODE]
|
||||
@word_re = (MIME_HEADERS[scantype] ? token : atom)
|
||||
end
|
||||
|
||||
attr_accessor :debug
|
||||
|
||||
def scan( &block )
|
||||
if @debug
|
||||
scan_main do |arr|
|
||||
s, v = arr
|
||||
printf "%7d %-10s %s\n",
|
||||
rest_size(),
|
||||
s.respond_to?(:id2name) ? s.id2name : s.inspect,
|
||||
v.inspect
|
||||
yield arr
|
||||
end
|
||||
else
|
||||
scan_main(&block)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
RECV_TOKEN = {
|
||||
'from' => :FROM,
|
||||
'by' => :BY,
|
||||
'via' => :VIA,
|
||||
'with' => :WITH,
|
||||
'id' => :ID,
|
||||
'for' => :FOR
|
||||
}
|
||||
|
||||
def scan_main
|
||||
until eof?
|
||||
if skip(/\A[\n\r\t ]+/n) # LWSP
|
||||
break if eof?
|
||||
end
|
||||
|
||||
if s = readstr(@word_re)
|
||||
if @is_mime_header
|
||||
yield [:TOKEN, s]
|
||||
else
|
||||
# atom
|
||||
if /\A\d+\z/ === s
|
||||
yield [:DIGIT, s]
|
||||
elsif @received
|
||||
yield [RECV_TOKEN[s.downcase] || :ATOM, s]
|
||||
else
|
||||
yield [:ATOM, s]
|
||||
end
|
||||
end
|
||||
|
||||
elsif skip(/\A"/)
|
||||
yield [:QUOTED, scan_quoted_word()]
|
||||
|
||||
elsif skip(/\A\[/)
|
||||
yield [:DOMLIT, scan_domain_literal()]
|
||||
|
||||
elsif skip(/\A\(/)
|
||||
@comments.push scan_comment()
|
||||
|
||||
else
|
||||
c = readchar()
|
||||
yield [c, c]
|
||||
end
|
||||
end
|
||||
|
||||
yield [false, '$']
|
||||
end
|
||||
|
||||
def scan_quoted_word
|
||||
scan_qstr(@quoted_re, /\A"/, 'quoted-word')
|
||||
end
|
||||
|
||||
def scan_domain_literal
|
||||
'[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
|
||||
end
|
||||
|
||||
def scan_qstr( pattern, terminal, type )
|
||||
result = ''
|
||||
until eof?
|
||||
if s = readstr(pattern) then result << s
|
||||
elsif skip(terminal) then return result
|
||||
elsif skip(/\A\\/) then result << readchar()
|
||||
else
|
||||
raise "TMail FATAL: not match in #{type}"
|
||||
end
|
||||
end
|
||||
scan_error! "found unterminated #{type}"
|
||||
end
|
||||
|
||||
def scan_comment
|
||||
result = ''
|
||||
nest = 1
|
||||
content = @comment_re
|
||||
|
||||
until eof?
|
||||
if s = readstr(content) then result << s
|
||||
elsif skip(/\A\)/) then nest -= 1
|
||||
return result if nest == 0
|
||||
result << ')'
|
||||
elsif skip(/\A\(/) then nest += 1
|
||||
result << '('
|
||||
elsif skip(/\A\\/) then result << readchar()
|
||||
else
|
||||
raise 'TMail FATAL: not match in comment'
|
||||
end
|
||||
end
|
||||
scan_error! 'found unterminated comment'
|
||||
end
|
||||
|
||||
# string scanner
|
||||
|
||||
def init_scanner( str )
|
||||
@src = str
|
||||
end
|
||||
|
||||
def eof?
|
||||
@src.empty?
|
||||
end
|
||||
|
||||
def rest_size
|
||||
@src.size
|
||||
end
|
||||
|
||||
def readstr( re )
|
||||
if m = re.match(@src)
|
||||
@src = m.post_match
|
||||
m[0]
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def readchar
|
||||
readstr(/\A./)
|
||||
end
|
||||
|
||||
def skip( re )
|
||||
if m = re.match(@src)
|
||||
@src = m.post_match
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def scan_error!( msg )
|
||||
raise SyntaxError, msg
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
||||
#:startdoc:
|
||||
@@ -1,280 +0,0 @@
|
||||
# encoding: utf-8
|
||||
=begin rdoc
|
||||
|
||||
= String handling class
|
||||
|
||||
=end
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
class StringInput#:nodoc:
|
||||
|
||||
include Enumerable
|
||||
|
||||
class << self
|
||||
|
||||
def new( str )
|
||||
if block_given?
|
||||
begin
|
||||
f = super
|
||||
yield f
|
||||
ensure
|
||||
f.close if f
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias open new
|
||||
|
||||
end
|
||||
|
||||
def initialize( str )
|
||||
@src = str
|
||||
@pos = 0
|
||||
@closed = false
|
||||
@lineno = 0
|
||||
end
|
||||
|
||||
attr_reader :lineno
|
||||
|
||||
def string
|
||||
@src
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>"
|
||||
end
|
||||
|
||||
def close
|
||||
stream_check!
|
||||
@pos = nil
|
||||
@closed = true
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def pos
|
||||
stream_check!
|
||||
[@pos, @src.size].min
|
||||
end
|
||||
|
||||
alias tell pos
|
||||
|
||||
def seek( offset, whence = IO::SEEK_SET )
|
||||
stream_check!
|
||||
case whence
|
||||
when IO::SEEK_SET
|
||||
@pos = offset
|
||||
when IO::SEEK_CUR
|
||||
@pos += offset
|
||||
when IO::SEEK_END
|
||||
@pos = @src.size - offset
|
||||
else
|
||||
raise ArgumentError, "unknown seek flag: #{whence}"
|
||||
end
|
||||
@pos = 0 if @pos < 0
|
||||
@pos = [@pos, @src.size + 1].min
|
||||
offset
|
||||
end
|
||||
|
||||
def rewind
|
||||
stream_check!
|
||||
@pos = 0
|
||||
end
|
||||
|
||||
def eof?
|
||||
stream_check!
|
||||
@pos > @src.size
|
||||
end
|
||||
|
||||
def each( &block )
|
||||
stream_check!
|
||||
begin
|
||||
@src.each(&block)
|
||||
ensure
|
||||
@pos = 0
|
||||
end
|
||||
end
|
||||
|
||||
def gets
|
||||
stream_check!
|
||||
if idx = @src.index(?\n, @pos)
|
||||
idx += 1 # "\n".size
|
||||
line = @src[ @pos ... idx ]
|
||||
@pos = idx
|
||||
@pos += 1 if @pos == @src.size
|
||||
else
|
||||
line = @src[ @pos .. -1 ]
|
||||
@pos = @src.size + 1
|
||||
end
|
||||
@lineno += 1
|
||||
|
||||
line
|
||||
end
|
||||
|
||||
def getc
|
||||
stream_check!
|
||||
ch = @src[@pos]
|
||||
@pos += 1
|
||||
@pos += 1 if @pos == @src.size
|
||||
ch
|
||||
end
|
||||
|
||||
def read( len = nil )
|
||||
stream_check!
|
||||
return read_all unless len
|
||||
str = @src[@pos, len]
|
||||
@pos += len
|
||||
@pos += 1 if @pos == @src.size
|
||||
str
|
||||
end
|
||||
|
||||
alias sysread read
|
||||
|
||||
def read_all
|
||||
stream_check!
|
||||
return nil if eof?
|
||||
rest = @src[@pos ... @src.size]
|
||||
@pos = @src.size + 1
|
||||
rest
|
||||
end
|
||||
|
||||
def stream_check!
|
||||
@closed and raise IOError, 'closed stream'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class StringOutput#:nodoc:
|
||||
|
||||
class << self
|
||||
|
||||
def new( str = '' )
|
||||
if block_given?
|
||||
begin
|
||||
f = super
|
||||
yield f
|
||||
ensure
|
||||
f.close if f
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias open new
|
||||
|
||||
end
|
||||
|
||||
def initialize( str = '' )
|
||||
@dest = str
|
||||
@closed = false
|
||||
end
|
||||
|
||||
def close
|
||||
@closed = true
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def string
|
||||
@dest
|
||||
end
|
||||
|
||||
alias value string
|
||||
alias to_str string
|
||||
|
||||
def size
|
||||
@dest.size
|
||||
end
|
||||
|
||||
alias pos size
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:#{@dest ? 'open' : 'closed'},#{object_id}>"
|
||||
end
|
||||
|
||||
def print( *args )
|
||||
stream_check!
|
||||
raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty?
|
||||
args.each do |s|
|
||||
raise ArgumentError, 'nil not allowed' if s.nil?
|
||||
@dest << s.to_s
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def puts( *args )
|
||||
stream_check!
|
||||
args.each do |str|
|
||||
@dest << (s = str.to_s)
|
||||
@dest << "\n" unless s[-1] == ?\n
|
||||
end
|
||||
@dest << "\n" if args.empty?
|
||||
nil
|
||||
end
|
||||
|
||||
def putc( ch )
|
||||
stream_check!
|
||||
@dest << ch.chr
|
||||
nil
|
||||
end
|
||||
|
||||
def printf( *args )
|
||||
stream_check!
|
||||
@dest << sprintf(*args)
|
||||
nil
|
||||
end
|
||||
|
||||
def write( str )
|
||||
stream_check!
|
||||
s = str.to_s
|
||||
@dest << s
|
||||
s.size
|
||||
end
|
||||
|
||||
alias syswrite write
|
||||
|
||||
def <<( str )
|
||||
stream_check!
|
||||
@dest << str.to_s
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stream_check!
|
||||
@closed and raise IOError, 'closed stream'
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,337 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
# = TMail - The EMail Swiss Army Knife for Ruby
|
||||
#
|
||||
# The TMail library provides you with a very complete way to handle and manipulate EMails
|
||||
# from within your Ruby programs.
|
||||
#
|
||||
# Used as the backbone for email handling by the Ruby on Rails and Nitro web frameworks as
|
||||
# well as a bunch of other Ruby apps including the Ruby-Talk mailing list to newsgroup email
|
||||
# gateway, it is a proven and reliable email handler that won't let you down.
|
||||
#
|
||||
# Originally created by Minero Aoki, TMail has been recently picked up by Mikel Lindsaar and
|
||||
# is being actively maintained. Numerous backlogged bug fixes have been applied as well as
|
||||
# Ruby 1.9 compatibility and a swath of documentation to boot.
|
||||
#
|
||||
# TMail allows you to treat an email totally as an object and allow you to get on with your
|
||||
# own programming without having to worry about crafting the perfect email address validation
|
||||
# parser, or assembling an email from all it's component parts.
|
||||
#
|
||||
# TMail handles the most complex part of the email - the header. It generates and parses
|
||||
# headers and provides you with instant access to their innards through simple and logically
|
||||
# named accessor and setter methods.
|
||||
#
|
||||
# TMail also provides a wrapper to Net/SMTP as well as Unix Mailbox handling methods to
|
||||
# directly read emails from your unix mailbox, parse them and use them.
|
||||
#
|
||||
# Following is the comprehensive list of methods to access TMail::Mail objects. You can also
|
||||
# check out TMail::Mail, TMail::Address and TMail::Headers for other lists.
|
||||
module TMail
|
||||
|
||||
# Provides an exception to throw on errors in Syntax within TMail's parsers
|
||||
class SyntaxError < StandardError; end
|
||||
|
||||
# Provides a new email boundary to separate parts of the email. This is a random
|
||||
# string based off the current time, so should be fairly unique.
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# TMail.new_boundary
|
||||
# #=> "mimepart_47bf656968207_25a8fbb80114"
|
||||
# TMail.new_boundary
|
||||
# #=> "mimepart_47bf66051de4_25a8fbb80240"
|
||||
def TMail.new_boundary
|
||||
'mimepart_' + random_tag
|
||||
end
|
||||
|
||||
# Provides a new email message ID. You can use this to generate unique email message
|
||||
# id's for your email so you can track them.
|
||||
#
|
||||
# Optionally takes a fully qualified domain name (default to the current hostname
|
||||
# returned by Socket.gethostname) that will be appended to the message ID.
|
||||
#
|
||||
# For Example:
|
||||
#
|
||||
# email.message_id = TMail.new_message_id
|
||||
# #=> "<47bf66845380e_25a8fbb80332@baci.local.tmail>"
|
||||
# email.to_s
|
||||
# #=> "Message-Id: <47bf668b633f1_25a8fbb80475@baci.local.tmail>\n\n"
|
||||
# email.message_id = TMail.new_message_id("lindsaar.net")
|
||||
# #=> "<47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>"
|
||||
# email.to_s
|
||||
# #=> "Message-Id: <47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>\n\n"
|
||||
def TMail.new_message_id( fqdn = nil )
|
||||
fqdn ||= ::Socket.gethostname
|
||||
"<#{random_tag()}@#{fqdn}.tmail>"
|
||||
end
|
||||
|
||||
#:stopdoc:
|
||||
def TMail.random_tag #:nodoc:
|
||||
@uniq += 1
|
||||
t = Time.now
|
||||
sprintf('%x%x_%x%x%d%x',
|
||||
t.to_i, t.tv_usec,
|
||||
$$, Thread.current.object_id, @uniq, rand(255))
|
||||
end
|
||||
private_class_method :random_tag
|
||||
|
||||
@uniq = 0
|
||||
|
||||
#:startdoc:
|
||||
|
||||
# Text Utils provides a namespace to define TOKENs, ATOMs, PHRASEs and CONTROL characters that
|
||||
# are OK per RFC 2822.
|
||||
#
|
||||
# It also provides methods you can call to determine if a string is safe
|
||||
module TextUtils
|
||||
|
||||
aspecial = %Q|()<>[]:;.\\,"|
|
||||
tspecial = %Q|()<>[];:\\,"/?=|
|
||||
lwsp = %Q| \t\r\n|
|
||||
control = %Q|\x00-\x1f\x7f-\xff|
|
||||
|
||||
CONTROL_CHAR = /[#{control}]/n
|
||||
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
|
||||
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
|
||||
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
|
||||
|
||||
# Returns true if the string supplied is free from characters not allowed as an ATOM
|
||||
def atom_safe?( str )
|
||||
not ATOM_UNSAFE === str
|
||||
end
|
||||
|
||||
# If the string supplied has ATOM unsafe characters in it, will return the string quoted
|
||||
# in double quotes, otherwise returns the string unmodified
|
||||
def quote_atom( str )
|
||||
(ATOM_UNSAFE === str) ? dquote(str) : str
|
||||
end
|
||||
|
||||
# If the string supplied has PHRASE unsafe characters in it, will return the string quoted
|
||||
# in double quotes, otherwise returns the string unmodified
|
||||
def quote_phrase( str )
|
||||
(PHRASE_UNSAFE === str) ? dquote(str) : str
|
||||
end
|
||||
|
||||
# Returns true if the string supplied is free from characters not allowed as a TOKEN
|
||||
def token_safe?( str )
|
||||
not TOKEN_UNSAFE === str
|
||||
end
|
||||
|
||||
# If the string supplied has TOKEN unsafe characters in it, will return the string quoted
|
||||
# in double quotes, otherwise returns the string unmodified
|
||||
def quote_token( str )
|
||||
(TOKEN_UNSAFE === str) ? dquote(str) : str
|
||||
end
|
||||
|
||||
# Wraps supplied string in double quotes unless it is already wrapped
|
||||
# Returns double quoted string
|
||||
def dquote( str ) #:nodoc:
|
||||
unless str =~ /^".*?"$/
|
||||
'"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"'
|
||||
else
|
||||
str
|
||||
end
|
||||
end
|
||||
private :dquote
|
||||
|
||||
# Unwraps supplied string from inside double quotes
|
||||
# Returns unquoted string
|
||||
def unquote( str )
|
||||
str =~ /^"(.*?)"$/ ? $1 : str
|
||||
end
|
||||
|
||||
# Provides a method to join a domain name by it's parts and also makes it
|
||||
# ATOM safe by quoting it as needed
|
||||
def join_domain( arr )
|
||||
arr.map {|i|
|
||||
if /\A\[.*\]\z/ === i
|
||||
i
|
||||
else
|
||||
quote_atom(i)
|
||||
end
|
||||
}.join('.')
|
||||
end
|
||||
|
||||
#:stopdoc:
|
||||
ZONESTR_TABLE = {
|
||||
'jst' => 9 * 60,
|
||||
'eet' => 2 * 60,
|
||||
'bst' => 1 * 60,
|
||||
'met' => 1 * 60,
|
||||
'gmt' => 0,
|
||||
'utc' => 0,
|
||||
'ut' => 0,
|
||||
'nst' => -(3 * 60 + 30),
|
||||
'ast' => -4 * 60,
|
||||
'edt' => -4 * 60,
|
||||
'est' => -5 * 60,
|
||||
'cdt' => -5 * 60,
|
||||
'cst' => -6 * 60,
|
||||
'mdt' => -6 * 60,
|
||||
'mst' => -7 * 60,
|
||||
'pdt' => -7 * 60,
|
||||
'pst' => -8 * 60,
|
||||
'a' => -1 * 60,
|
||||
'b' => -2 * 60,
|
||||
'c' => -3 * 60,
|
||||
'd' => -4 * 60,
|
||||
'e' => -5 * 60,
|
||||
'f' => -6 * 60,
|
||||
'g' => -7 * 60,
|
||||
'h' => -8 * 60,
|
||||
'i' => -9 * 60,
|
||||
# j not use
|
||||
'k' => -10 * 60,
|
||||
'l' => -11 * 60,
|
||||
'm' => -12 * 60,
|
||||
'n' => 1 * 60,
|
||||
'o' => 2 * 60,
|
||||
'p' => 3 * 60,
|
||||
'q' => 4 * 60,
|
||||
'r' => 5 * 60,
|
||||
's' => 6 * 60,
|
||||
't' => 7 * 60,
|
||||
'u' => 8 * 60,
|
||||
'v' => 9 * 60,
|
||||
'w' => 10 * 60,
|
||||
'x' => 11 * 60,
|
||||
'y' => 12 * 60,
|
||||
'z' => 0 * 60
|
||||
}
|
||||
#:startdoc:
|
||||
|
||||
# Takes a time zone string from an EMail and converts it to Unix Time (seconds)
|
||||
def timezone_string_to_unixtime( str )
|
||||
if m = /([\+\-])(\d\d?)(\d\d)/.match(str)
|
||||
sec = (m[2].to_i * 60 + m[3].to_i) * 60
|
||||
m[1] == '-' ? -sec : sec
|
||||
else
|
||||
min = ZONESTR_TABLE[str.downcase] or
|
||||
raise SyntaxError, "wrong timezone format '#{str}'"
|
||||
min * 60
|
||||
end
|
||||
end
|
||||
|
||||
#:stopdoc:
|
||||
WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG )
|
||||
MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun
|
||||
Jul Aug Sep Oct Nov Dec TMailBUG )
|
||||
|
||||
def time2str( tm )
|
||||
# [ruby-list:7928]
|
||||
gmt = Time.at(tm.to_i)
|
||||
gmt.gmtime
|
||||
offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i
|
||||
|
||||
# DO NOT USE strftime: setlocale() breaks it
|
||||
sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
|
||||
WDAY[tm.wday], tm.mday, MONTH[tm.month],
|
||||
tm.year, tm.hour, tm.min, tm.sec,
|
||||
*(offset / 60).divmod(60)
|
||||
end
|
||||
|
||||
|
||||
MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/
|
||||
|
||||
def message_id?( str )
|
||||
MESSAGE_ID === str
|
||||
end
|
||||
|
||||
|
||||
MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
|
||||
|
||||
def mime_encoded?( str )
|
||||
MIME_ENCODED === str
|
||||
end
|
||||
|
||||
|
||||
def decode_params( hash )
|
||||
new = Hash.new
|
||||
encoded = nil
|
||||
hash.each do |key, value|
|
||||
if m = /\*(?:(\d+)\*)?\z/.match(key)
|
||||
((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value
|
||||
else
|
||||
new[key] = to_kcode(value)
|
||||
end
|
||||
end
|
||||
if encoded
|
||||
encoded.each do |key, strings|
|
||||
new[key] = decode_RFC2231(strings.join(''))
|
||||
end
|
||||
end
|
||||
|
||||
new
|
||||
end
|
||||
|
||||
NKF_FLAGS = {
|
||||
'EUC' => '-e -m',
|
||||
'SJIS' => '-s -m'
|
||||
}
|
||||
|
||||
def to_kcode( str )
|
||||
flag = NKF_FLAGS[TMail.KCODE] or return str
|
||||
NKF.nkf(flag, str)
|
||||
end
|
||||
|
||||
RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
|
||||
|
||||
def decode_RFC2231( str )
|
||||
m = RFC2231_ENCODED.match(str) or return str
|
||||
begin
|
||||
to_kcode(m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
|
||||
rescue
|
||||
m.post_match.gsub(/%[\da-f]{2}/in, "")
|
||||
end
|
||||
end
|
||||
|
||||
def quote_boundary
|
||||
# Make sure the Content-Type boundary= parameter is quoted if it contains illegal characters
|
||||
# (to ensure any special characters in the boundary text are escaped from the parser
|
||||
# (such as = in MS Outlook's boundary text))
|
||||
if @body =~ /^(.*)boundary=(.*)$/m
|
||||
preamble = $1
|
||||
remainder = $2
|
||||
if remainder =~ /;/
|
||||
remainder =~ /^(.*?)(;.*)$/m
|
||||
boundary_text = $1
|
||||
post = $2.chomp
|
||||
else
|
||||
boundary_text = remainder.chomp
|
||||
end
|
||||
if boundary_text =~ /[\/\?\=]/
|
||||
boundary_text = "\"#{boundary_text}\"" unless boundary_text =~ /^".*?"$/
|
||||
@body = "#{preamble}boundary=#{boundary_text}#{post}"
|
||||
end
|
||||
end
|
||||
end
|
||||
#:startdoc:
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
#
|
||||
# version.rb
|
||||
#
|
||||
#--
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
||||
# with permission of Minero Aoki.
|
||||
#++
|
||||
|
||||
#:stopdoc:
|
||||
module TMail
|
||||
module VERSION
|
||||
MAJOR = 1
|
||||
MINOR = 2
|
||||
TINY = 3
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
17
actionmailer/lib/action_mailer/vendor/tmail.rb
vendored
17
actionmailer/lib/action_mailer/vendor/tmail.rb
vendored
@@ -1,17 +0,0 @@
|
||||
# Prefer gems to the bundled libs.
|
||||
require 'rubygems'
|
||||
|
||||
begin
|
||||
gem 'tmail', '~> 1.2.3'
|
||||
rescue Gem::LoadError
|
||||
$:.unshift "#{File.dirname(__FILE__)}/tmail-1.2.3"
|
||||
end
|
||||
|
||||
module TMail
|
||||
end
|
||||
|
||||
require 'tmail'
|
||||
|
||||
silence_warnings do
|
||||
TMail::Encoder.const_set("MAX_LINE_LEN", 200)
|
||||
end
|
||||
@@ -2,7 +2,7 @@ module ActionMailer
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 2
|
||||
TINY = 14
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
require 'action_mailer'
|
||||
ActiveSupport::Deprecation.warn 'require "actionmailer" is deprecated and will be removed in Rails 3. Use require "action_mailer" instead.'
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
|
||||
gem 'mocha', '>= 0.9.5'
|
||||
require 'mocha'
|
||||
|
||||
$:.unshift "#{File.dirname(__FILE__)}/../lib"
|
||||
$:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
$:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib"
|
||||
$:.unshift File.expand_path('../../lib', __FILE__)
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
$:.unshift File.expand_path('../../../actionpack/lib', __FILE__)
|
||||
require 'action_mailer'
|
||||
require 'action_mailer/test_case'
|
||||
|
||||
@@ -20,7 +17,7 @@ ActionView::Template.register_template_handler :bak, lambda { |template| "Lame b
|
||||
$:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers"
|
||||
|
||||
ActionView::Base.cache_template_loading = true
|
||||
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
|
||||
FIXTURE_LOAD_PATH = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures'))
|
||||
ActionMailer::Base.template_root = FIXTURE_LOAD_PATH
|
||||
|
||||
class MockSMTP
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module ExampleHelper
|
||||
def example_format(text)
|
||||
"<em><strong><small>#{text}</small></strong></em>"
|
||||
"<em><strong><small>#{h(text)}</small></strong></em>".html_safe
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,6 @@ class TestMailer < ActionMailer::Base
|
||||
@recipients = recipient
|
||||
@subject = "[Signed up] Welcome #{recipient}"
|
||||
@from = "system@loudthinking.com"
|
||||
@sent_on = Time.local(2004, 12, 12)
|
||||
@body["recipient"] = recipient
|
||||
end
|
||||
|
||||
@@ -30,6 +29,18 @@ class TestMailer < ActionMailer::Base
|
||||
self.body = "Goodbye, Mr. #{recipient}"
|
||||
end
|
||||
|
||||
def from_with_name
|
||||
from "System <system@loudthinking.com>"
|
||||
recipients "root@loudthinking.com"
|
||||
body "Nothing to see here."
|
||||
end
|
||||
|
||||
def from_without_name
|
||||
from "system@loudthinking.com"
|
||||
recipients "root@loudthinking.com"
|
||||
body "Nothing to see here."
|
||||
end
|
||||
|
||||
def cc_bcc(recipient)
|
||||
recipients recipient
|
||||
subject "testing bcc/cc"
|
||||
@@ -356,12 +367,14 @@ class ActionMailerTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_signed_up
|
||||
Time.stubs(:now => Time.now)
|
||||
|
||||
expected = new_mail
|
||||
expected.to = @recipient
|
||||
expected.subject = "[Signed up] Welcome #{@recipient}"
|
||||
expected.body = "Hello there, \n\nMr. #{@recipient}"
|
||||
expected.from = "system@loudthinking.com"
|
||||
expected.date = Time.local(2004, 12, 12)
|
||||
expected.date = Time.now
|
||||
|
||||
created = nil
|
||||
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
|
||||
@@ -453,6 +466,28 @@ class ActionMailerTest < Test::Unit::TestCase
|
||||
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
|
||||
end
|
||||
|
||||
def test_from_without_name_for_smtp
|
||||
ActionMailer::Base.delivery_method = :smtp
|
||||
TestMailer.deliver_from_without_name
|
||||
|
||||
mail = MockSMTP.deliveries.first
|
||||
assert_not_nil mail
|
||||
mail, from, to = mail
|
||||
|
||||
assert_equal 'system@loudthinking.com', from.to_s
|
||||
end
|
||||
|
||||
def test_from_with_name_for_smtp
|
||||
ActionMailer::Base.delivery_method = :smtp
|
||||
TestMailer.deliver_from_with_name
|
||||
|
||||
mail = MockSMTP.deliveries.first
|
||||
assert_not_nil mail
|
||||
mail, from, to = mail
|
||||
|
||||
assert_equal 'system@loudthinking.com', from.to_s
|
||||
end
|
||||
|
||||
def test_reply_to
|
||||
expected = new_mail
|
||||
|
||||
@@ -569,7 +604,9 @@ class ActionMailerTest < Test::Unit::TestCase
|
||||
mail = TestMailer.create_signed_up(@recipient)
|
||||
logger = mock()
|
||||
logger.expects(:info).with("Sent mail to #{@recipient}")
|
||||
logger.expects(:debug).with("\n#{mail.encoded}")
|
||||
logger.expects(:debug).with() do |logged_text|
|
||||
logged_text =~ /\[Signed up\] Welcome/
|
||||
end
|
||||
TestMailer.logger = logger
|
||||
TestMailer.deliver_signed_up(@recipient)
|
||||
end
|
||||
@@ -697,8 +734,8 @@ EOF
|
||||
expected.date = Time.local 2004, 12, 12
|
||||
|
||||
created = TestMailer.create_utf8_body @recipient
|
||||
assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>\r/, created.encoded)
|
||||
assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>, Example Recipient <me/, created.encoded)
|
||||
assert_match(/From:\ =\?utf\-8\?Q\?Foo_=C3=A1=C3=AB=C3=B4_=C3=AE=C3=BC\?=\ <extended@example\.net>/, created.encoded)
|
||||
assert_match(/To:\ =\?utf\-8\?Q\?Foo_=C3=A1=C3=AB=C3=B4_=C3=AE=C3=BC\?=\ <extended@example\.net>/, created.encoded)
|
||||
end
|
||||
|
||||
def test_receive_decodes_base64_encoded_mail
|
||||
@@ -824,6 +861,26 @@ EOF
|
||||
assert_equal "text/yaml", mail.parts[2].content_type
|
||||
end
|
||||
|
||||
def test_implicitly_path_when_running_from_none_rails_root
|
||||
exected_path = File.expand_path(File.join(File.dirname(__FILE__), "fixtures", "test_mailer"))
|
||||
with_a_rails_root do
|
||||
Dir.chdir "/" do
|
||||
template_path = TestMailer.allocate.send(:template_path)
|
||||
assert_equal exected_path, File.expand_path(template_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_implicitly_multipart_messages_run_from_another_location_with_a_rails_root
|
||||
with_a_rails_root do
|
||||
Dir.chdir "/" do
|
||||
mail = TestMailer.create_implicitly_multipart_example(@recipient)
|
||||
assert_equal 3, mail.parts.length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_implicitly_multipart_messages_with_charset
|
||||
mail = TestMailer.create_implicitly_multipart_example(@recipient, 'iso-8859-1')
|
||||
|
||||
@@ -972,6 +1029,16 @@ EOF
|
||||
ensure
|
||||
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
|
||||
end
|
||||
|
||||
private
|
||||
def with_a_rails_root
|
||||
old_root = ::RAILS_ROOT if defined? ::RAILS_ROOT
|
||||
Object.const_set(:RAILS_ROOT, File.join(File.dirname(__FILE__)))
|
||||
yield
|
||||
ensure
|
||||
Object.send(:remove_const, :RAILS_ROOT) if defined? ::RAILS_ROOT
|
||||
Object.const_set(:RAILS_ROOT, old_root) if old_root
|
||||
end
|
||||
end
|
||||
|
||||
class InheritableTemplateRootTest < Test::Unit::TestCase
|
||||
|
||||
@@ -23,9 +23,13 @@ class QuotingTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
def test_unqoute_multiple
|
||||
a ="=?utf-8?q?Re=3A_=5B12=5D_=23137=3A_Inkonsistente_verwendung_von_=22Hin?==?utf-8?b?enVmw7xnZW4i?="
|
||||
b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
|
||||
assert_equal "Re: [12] #137: Inkonsistente verwendung von \"Hinzuf\303\274gen\"", b
|
||||
quoted ="=?utf-8?q?Re=3A_=5B12=5D_=23137=3A_Inkonsistente_verwendung_von_=22Hin?==?utf-8?b?enVmw7xnZW4i?="
|
||||
actual = TMail::Unquoter.unquote_and_convert_to(quoted, 'utf-8')
|
||||
|
||||
expected = "Re: [12] #137: Inkonsistente verwendung von \"Hinzuf\303\274gen\""
|
||||
expected.force_encoding 'ASCII-8BIT' if expected.respond_to?(:force_encoding)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
def test_unqoute_in_the_middle
|
||||
@@ -71,7 +75,9 @@ class QuotingTest < Test::Unit::TestCase
|
||||
|
||||
def test_email_with_partially_quoted_subject
|
||||
mail = TMail::Mail.parse(IO.read("#{File.dirname(__FILE__)}/fixtures/raw_email_with_partially_quoted_subject"))
|
||||
assert_equal "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail", mail.subject
|
||||
expected = "Re: Test: \"\346\274\242\345\255\227\" mid \"\346\274\242\345\255\227\" tail"
|
||||
expected.force_encoding('ASCII-8BIT') if expected.respond_to?(:force_encoding)
|
||||
assert_equal expected, mail.subject
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,7 +1,101 @@
|
||||
*2.3.11 (February 9, 2011)*
|
||||
|
||||
* Two security fixes. CVE-2011-0446, CVE-2011-0447
|
||||
|
||||
*2.3.10 (October 15, 2010)*
|
||||
|
||||
*2.3.9 (September 4, 2010)*
|
||||
|
||||
* Version bump.
|
||||
|
||||
|
||||
*2.3.8 (May 24, 2010)*
|
||||
|
||||
* HTML safety: fix compatibility *without* the optional rails_xss plugin.
|
||||
|
||||
|
||||
*2.3.7 (May 24, 2010)*
|
||||
|
||||
* HTML safety: fix compatibility with the optional rails_xss plugin. [Nathan Weizenbaum, Santiago Pastorino]
|
||||
|
||||
|
||||
*2.3.6 (May 23, 2010)*
|
||||
|
||||
* JSON: set Base.include_root_in_json = true to include a root value in the JSON: {"post": {"title": ...}}. Mirrors the Active Record option. #2584 [Matthew Moore, Joe Martinez, Elad Meidar, Santiago Pastorino]
|
||||
|
||||
* Ruby 1.9: ERB template encoding using a magic comment at the top of the file. [Jeremy Kemper]
|
||||
<%# encoding: utf-8 %>
|
||||
|
||||
* Fixed that default locale templates should be used if the current locale template is missing [DHH]
|
||||
|
||||
* Fixed that PrototypeHelper#update_page should return html_safe [DHH]
|
||||
|
||||
* Fixed that much of DateHelper wouldn't return html_safe? strings [DHH]
|
||||
|
||||
* Fixed that fragment caching should return a cache hit as html_safe (or it would all just get escaped) [DHH]
|
||||
|
||||
* Introduce String#html_safe for rails_xss plugin and forward-compatibility with Rails 3. [Michael Koziarski, Santiago Pastorino, José Ignacio Costa]
|
||||
|
||||
* Added :alert, :notice, and :flash as options to ActionController::Base#redirect_to that'll automatically set the proper flash before the redirection [DHH]. Examples:
|
||||
|
||||
flash[:notice] = 'Post was created'
|
||||
redirect_to(@post)
|
||||
|
||||
...becomes:
|
||||
|
||||
redirect_to(@post, :notice => 'Post was created')
|
||||
|
||||
* Added ActionController::Base#notice/= and ActionController::Base#alert/= as a convenience accessors in both the controller and the view for flash[:notice]/= and flash[:alert]/= [DHH]
|
||||
|
||||
* Added cookies.permanent, cookies.signed, and cookies.permanent.signed accessor for common cookie actions [DHH]. Examples:
|
||||
|
||||
cookies.permanent[:prefers_open_id] = true
|
||||
# => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
|
||||
cookies.signed[:discount] = 45
|
||||
# => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
|
||||
|
||||
cookies.signed[:discount]
|
||||
# => 45 (if the cookie was changed, you'll get a InvalidSignature exception)
|
||||
|
||||
cookies.permanent.signed[:remember_me] = current_user.id
|
||||
# => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
|
||||
...to use the signed cookies, you need to set a secret to ActionController::Base.cookie_verifier_secret (automatically done in config/initializers/cookie_verification_secret.rb for new Rails applications).
|
||||
|
||||
|
||||
*2.3.5 (November 25, 2009)*
|
||||
|
||||
* Minor Bug Fixes and deprecation warnings
|
||||
|
||||
* Ruby 1.9 Support
|
||||
|
||||
* Fix filtering parameters when there are Fixnum or other un-dupable values.
|
||||
|
||||
* Improvements to ActionView::TestCase
|
||||
|
||||
* Compatiblity with the rails_xss plugin
|
||||
|
||||
*2.3.4 (September 4, 2009)*
|
||||
|
||||
* Sanitize multibyte strings before escaping them with escape_once. CVE-2009-3009
|
||||
|
||||
* Introduce grouped_collection_select helper. #1249 [Dan Codeape, Erik Ostrom]
|
||||
|
||||
* Ruby 1.9: fix Content-Length for multibyte send_data streaming. #2661 [Sava Chankov]
|
||||
|
||||
|
||||
*2.3.3 (July 12, 2009)*
|
||||
|
||||
* Fixed that TestResponse.cookies was returning cookies unescaped #1867 [Doug McInnes]
|
||||
|
||||
|
||||
*2.3.2 [Final] (March 15, 2009)*
|
||||
|
||||
* Fixed that redirection would just log the options, not the final url (which lead to "Redirected to #<Post:0x23150b8>") [DHH]
|
||||
|
||||
* Don't check authenticity tokens for any AJAX requests [Ross Kaffenberger/Bryan Helmkamp]
|
||||
|
||||
* Added ability to pass in :public => true to fresh_when, stale?, and expires_in to make the request proxy cachable #2095 [Gregg Pollack]
|
||||
|
||||
* Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger]
|
||||
@@ -1841,7 +1935,7 @@ superclass' view_paths. [Rick Olson]
|
||||
|
||||
* Update documentation for erb trim syntax. #5651 [matt@mattmargolis.net]
|
||||
|
||||
* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com, sebastien@goetzilla.info]
|
||||
* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com]
|
||||
|
||||
* Reset @html_document between requests so assert_tag works. #4810 [Jarkko Laine, easleydp@gmail.com]
|
||||
|
||||
@@ -2438,7 +2532,7 @@ superclass' view_paths. [Rick Olson]
|
||||
|
||||
* Provide support for decimal columns to form helpers. Closes #5672. [Dave Thomas]
|
||||
|
||||
* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com, sebastien@goetzilla.info]
|
||||
* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com]
|
||||
|
||||
* Reset @html_document between requests so assert_tag works. #4810 [Jarkko Laine, easleydp@gmail.com]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -155,20 +155,20 @@ A short rundown of the major features:
|
||||
map.connect 'clients/:client_name/:project_name/:controller/:action'
|
||||
|
||||
Accessing /clients/37signals/basecamp/project/dash calls ProjectController#dash with
|
||||
{ "client_name" => "37signals", "project_name" => "basecamp" } in params[:params]
|
||||
{ "client_name" => "37signals", "project_name" => "basecamp" }.
|
||||
|
||||
From that URL, you can rewrite the redirect in a number of ways:
|
||||
From that URL you can redirect providing new parameters in a number of ways:
|
||||
|
||||
redirect_to(:action => "edit") =>
|
||||
/clients/37signals/basecamp/project/dash
|
||||
redirect_to :action => "edit"
|
||||
# /clients/37signals/basecamp/project/edit
|
||||
|
||||
redirect_to(:client_name => "nextangle", :project_name => "rails") =>
|
||||
/clients/nextangle/rails/project/dash
|
||||
redirect_to :client_name => "nextangle", :project_name => "rails"
|
||||
# /clients/nextangle/rails/project/dash
|
||||
|
||||
{Learn more}[link:classes/ActionController/Base.html]
|
||||
|
||||
|
||||
* Javascript and Ajax integration
|
||||
* JavaScript and Ajax integration
|
||||
|
||||
link_to_function "Greeting", "alert('Hello world!')"
|
||||
link_to_remote "Delete this post", :update => "posts",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rdoc/task'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/sshpublisher'
|
||||
require 'rubygems/package_task'
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version')
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
@@ -30,7 +29,7 @@ Rake::TestTask.new(:test_action_pack) do |t|
|
||||
|
||||
# make sure we include the tests in alphabetical order as on some systems
|
||||
# this will not happen automatically and the tests (as a whole) will error
|
||||
t.test_files = Dir.glob( "test/[cft]*/**/*_test.rb" ).sort
|
||||
t.test_files = Dir.glob( "test/[cftv]*/**/*_test.rb" ).sort
|
||||
|
||||
t.verbose = true
|
||||
#t.warning = true
|
||||
@@ -46,7 +45,7 @@ end
|
||||
|
||||
# Genereate the RDoc documentation
|
||||
|
||||
Rake::RDocTask.new { |rdoc|
|
||||
RDoc::Task.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Action Pack -- On rails from request to response"
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
@@ -77,13 +76,13 @@ spec = Gem::Specification.new do |s|
|
||||
s.rubyforge_project = "actionpack"
|
||||
s.homepage = "http://www.rubyonrails.org"
|
||||
|
||||
s.has_rdoc = true
|
||||
s.requirements << 'none'
|
||||
|
||||
s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
|
||||
s.add_dependency('activesupport', '= 2.3.14' + PKG_BUILD)
|
||||
s.add_dependency('erubis', '~> 2.7.0')
|
||||
s.add_dependency('rack', '~> 1.1')
|
||||
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'action_controller'
|
||||
|
||||
s.files = [ "Rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE" ]
|
||||
dist_dirs.each do |dir|
|
||||
@@ -91,7 +90,7 @@ spec = Gem::Specification.new do |s|
|
||||
end
|
||||
end
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
Gem::PackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
@@ -136,12 +135,14 @@ task :update_js => [ :update_scriptaculous ]
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pgem => [:package] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#--
|
||||
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
# Copyright (c) 2004-2010 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -31,12 +31,8 @@ rescue LoadError
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
gem 'rack', '~> 1.0.0'
|
||||
require 'rack'
|
||||
rescue Gem::LoadError
|
||||
require 'action_controller/vendor/rack-1.0/rack'
|
||||
end
|
||||
require 'rack'
|
||||
require 'action_controller/cgi_ext'
|
||||
|
||||
module ActionController
|
||||
# TODO: Review explicit to see if they will automatically be handled by
|
||||
@@ -45,7 +41,6 @@ module ActionController
|
||||
[Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
|
||||
end
|
||||
|
||||
autoload :AbstractRequest, 'action_controller/request'
|
||||
autoload :Base, 'action_controller/base'
|
||||
autoload :Benchmarking, 'action_controller/benchmarking'
|
||||
autoload :Caching, 'action_controller/caching'
|
||||
@@ -75,6 +70,7 @@ module ActionController
|
||||
autoload :SessionManagement, 'action_controller/session_management'
|
||||
autoload :StatusCodes, 'action_controller/status_codes'
|
||||
autoload :Streaming, 'action_controller/streaming'
|
||||
autoload :StringCoercion, 'action_controller/string_coercion'
|
||||
autoload :TestCase, 'action_controller/test_case'
|
||||
autoload :TestProcess, 'action_controller/test_process'
|
||||
autoload :Translation, 'action_controller/translation'
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
module ActionController
|
||||
module Assertions
|
||||
module DomAssertions
|
||||
def self.strip_whitespace!(nodes)
|
||||
nodes.reject! do |node|
|
||||
if node.is_a?(HTML::Text)
|
||||
node.content.strip!
|
||||
node.content.empty?
|
||||
else
|
||||
strip_whitespace! node.children
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
|
||||
#
|
||||
# ==== Examples
|
||||
@@ -12,13 +24,15 @@ module ActionController
|
||||
clean_backtrace do
|
||||
expected_dom = HTML::Document.new(expected).root
|
||||
actual_dom = HTML::Document.new(actual).root
|
||||
full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s)
|
||||
DomAssertions.strip_whitespace!(expected_dom.children)
|
||||
DomAssertions.strip_whitespace!(actual_dom.children)
|
||||
|
||||
full_message = build_message(message, "<?> expected but was\n<?>.", expected_dom.to_s, actual_dom.to_s)
|
||||
assert_block(full_message) { expected_dom == actual_dom }
|
||||
end
|
||||
end
|
||||
|
||||
# The negated form of +assert_dom_equivalent+.
|
||||
# The negated form of +assert_dom_equal+.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
@@ -29,8 +43,10 @@ module ActionController
|
||||
clean_backtrace do
|
||||
expected_dom = HTML::Document.new(expected).root
|
||||
actual_dom = HTML::Document.new(actual).root
|
||||
full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s)
|
||||
DomAssertions.strip_whitespace!(expected_dom.children)
|
||||
DomAssertions.strip_whitespace!(actual_dom.children)
|
||||
|
||||
full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s)
|
||||
assert_block(full_message) { expected_dom != actual_dom }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -63,7 +63,12 @@ module ActionController
|
||||
|
||||
# Support partial arguments for hash redirections
|
||||
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
|
||||
return true if options.all? {|(key, value)| @response.redirected_to[key] == value}
|
||||
if options.all? {|(key, value)| @response.redirected_to[key] == value}
|
||||
callstack = caller.dup
|
||||
callstack.slice!(0, 2)
|
||||
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", callstack)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
|
||||
@@ -82,6 +87,9 @@ module ActionController
|
||||
# # assert that the "new" view template was rendered
|
||||
# assert_template "new"
|
||||
#
|
||||
# # assert that the "new" view template was rendered with Symbol
|
||||
# assert_template :new
|
||||
#
|
||||
# # assert that the "_customer" partial was rendered twice
|
||||
# assert_template :partial => '_customer', :count => 2
|
||||
#
|
||||
@@ -91,7 +99,7 @@ module ActionController
|
||||
def assert_template(options = {}, message = nil)
|
||||
clean_backtrace do
|
||||
case options
|
||||
when NilClass, String
|
||||
when NilClass, String, Symbol
|
||||
rendered = @response.rendered[:template].to_s
|
||||
msg = build_message(message,
|
||||
"expecting <?> but rendering with <?>",
|
||||
@@ -100,7 +108,7 @@ module ActionController
|
||||
if options.nil?
|
||||
@response.rendered[:template].blank?
|
||||
else
|
||||
rendered.to_s.match(options)
|
||||
rendered.to_s.match(options.to_s)
|
||||
end
|
||||
end
|
||||
when Hash
|
||||
@@ -123,6 +131,8 @@ module ActionController
|
||||
assert @response.rendered[:partials].empty?,
|
||||
"Expected no partials to be rendered"
|
||||
end
|
||||
else
|
||||
raise ArgumentError
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -134,16 +144,25 @@ module ActionController
|
||||
end
|
||||
|
||||
def normalize_argument_to_redirection(fragment)
|
||||
after_routing = @controller.url_for(fragment)
|
||||
if after_routing =~ %r{^\w+://.*}
|
||||
after_routing
|
||||
else
|
||||
# FIXME - this should probably get removed.
|
||||
if after_routing.first != '/'
|
||||
after_routing = '/' + after_routing
|
||||
case fragment
|
||||
when %r{^\w[\w\d+.-]*:.*}
|
||||
fragment
|
||||
when String
|
||||
if fragment =~ %r{^\w[\w\d+.-]*:.*}
|
||||
fragment
|
||||
else
|
||||
if fragment !~ /^\//
|
||||
ActiveSupport::Deprecation.warn "Omitting the leading slash on a path with assert_redirected_to is deprecated. Use '/#{fragment}' instead.", caller(2)
|
||||
fragment = "/#{fragment}"
|
||||
end
|
||||
@request.protocol + @request.host_with_port + fragment
|
||||
end
|
||||
@request.protocol + @request.host_with_port + after_routing
|
||||
end
|
||||
when :back
|
||||
raise RedirectBackError unless refer = @request.headers["Referer"]
|
||||
refer
|
||||
else
|
||||
@controller.url_for(fragment)
|
||||
end.gsub(/[\r\n]/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ module ActionController
|
||||
#
|
||||
# Use +css_select+ to select elements without making an assertions, either
|
||||
# from the response HTML or elements selected by the enclosing assertion.
|
||||
#
|
||||
#
|
||||
# In addition to HTML responses, you can make the following assertions:
|
||||
# * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations.
|
||||
# * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
|
||||
@@ -24,6 +24,12 @@ module ActionController
|
||||
#
|
||||
# Also see HTML::Selector to learn how to use selectors.
|
||||
module SelectorAssertions
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@selected = nil
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# css_select(selector) => array
|
||||
# css_select(element, selector) => array
|
||||
@@ -53,8 +59,8 @@ module ActionController
|
||||
# end
|
||||
#
|
||||
# # Selects all list items in unordered lists
|
||||
# items = css_select("ul>li")
|
||||
#
|
||||
# items = css_select("ul>li")
|
||||
#
|
||||
# # Selects all form tags and then all inputs inside the form
|
||||
# forms = css_select("form")
|
||||
# forms.each do |form|
|
||||
@@ -212,7 +218,7 @@ module ActionController
|
||||
# Otherwise just operate on the response document.
|
||||
root = response_from_page_or_rjs
|
||||
end
|
||||
|
||||
|
||||
# First or second argument is the selector: string and we pass
|
||||
# all remaining arguments. Array and we pass the argument. Also
|
||||
# accepts selector itself.
|
||||
@@ -225,7 +231,7 @@ module ActionController
|
||||
selector = arg
|
||||
else raise ArgumentError, "Expecting a selector as the first argument"
|
||||
end
|
||||
|
||||
|
||||
# Next argument is used for equality tests.
|
||||
equals = {}
|
||||
case arg = args.shift
|
||||
@@ -315,10 +321,10 @@ module ActionController
|
||||
# Returns all matches elements.
|
||||
matches
|
||||
end
|
||||
|
||||
|
||||
def count_description(min, max) #:nodoc:
|
||||
pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
|
||||
|
||||
|
||||
if min && max && (max != min)
|
||||
"between #{min} and #{max} elements"
|
||||
elsif min && !(min == 1 && max == 1)
|
||||
@@ -327,7 +333,7 @@ module ActionController
|
||||
"at most #{max} #{pluralize['element', max]}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# :call-seq:
|
||||
# assert_select_rjs(id?) { |elements| ... }
|
||||
# assert_select_rjs(statement, id?) { |elements| ... }
|
||||
@@ -344,7 +350,7 @@ module ActionController
|
||||
# that update or insert an element with that identifier.
|
||||
#
|
||||
# Use the first argument to narrow down assertions to only statements
|
||||
# of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>,
|
||||
# of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>,
|
||||
# <tt>:show</tt>, <tt>:hide</tt>, <tt>:toggle</tt>, <tt>:remove</tt> and
|
||||
# <tt>:insert_html</tt>.
|
||||
#
|
||||
@@ -488,7 +494,7 @@ module ActionController
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
#
|
||||
# # Selects all paragraph tags from within the description of an RSS feed
|
||||
# assert_select_feed :rss, 2.0 do
|
||||
|
||||
@@ -491,9 +491,18 @@ module ActionController #:nodoc:
|
||||
filtered_parameters[key] = '[FILTERED]'
|
||||
elsif value.is_a?(Hash)
|
||||
filtered_parameters[key] = filter_parameters(value)
|
||||
elsif value.is_a?(Array)
|
||||
filtered_parameters[key] = value.collect do |item|
|
||||
case item
|
||||
when Hash, Array
|
||||
filter_parameters(item)
|
||||
else
|
||||
item
|
||||
end
|
||||
end
|
||||
elsif block_given?
|
||||
key = key.dup
|
||||
value = value.dup if value
|
||||
value = value.dup if value.duplicable?
|
||||
yield key, value
|
||||
filtered_parameters[key] = value
|
||||
else
|
||||
@@ -607,15 +616,6 @@ module ActionController #:nodoc:
|
||||
# displayed on:
|
||||
#
|
||||
# url_for :controller => 'posts', :action => nil
|
||||
#
|
||||
# If you explicitly want to create a URL that's almost the same as the current URL, you can do so using the
|
||||
# <tt>:overwrite_params</tt> options. Say for your posts you have different views for showing and printing them.
|
||||
# Then, in the show view, you get the URL for the print view like this
|
||||
#
|
||||
# url_for :overwrite_params => { :action => 'print' }
|
||||
#
|
||||
# This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt>
|
||||
# would have slashed-off the path components after the changed action.
|
||||
def url_for(options = {})
|
||||
options ||= {}
|
||||
case options
|
||||
@@ -810,7 +810,6 @@ module ActionController #:nodoc:
|
||||
# render :text => proc { |response, output|
|
||||
# 10_000_000.times do |i|
|
||||
# output.write("This is line #{i}\n")
|
||||
# output.flush
|
||||
# end
|
||||
# }
|
||||
#
|
||||
@@ -950,8 +949,9 @@ module ActionController #:nodoc:
|
||||
response.content_type ||= Mime::JS
|
||||
render_for_text(js, options[:status])
|
||||
|
||||
elsif json = options[:json]
|
||||
json = json.to_json unless json.is_a?(String)
|
||||
elsif options.include?(:json)
|
||||
json = options[:json]
|
||||
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
|
||||
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
|
||||
response.content_type ||= Mime::JSON
|
||||
render_for_text(json, options[:status])
|
||||
@@ -1083,13 +1083,24 @@ module ActionController #:nodoc:
|
||||
# The redirection happens as a "302 Moved" header unless otherwise specified.
|
||||
#
|
||||
# Examples:
|
||||
# redirect_to post_url(@post), :status=>:found
|
||||
# redirect_to :action=>'atom', :status=>:moved_permanently
|
||||
# redirect_to post_url(@post), :status=>301
|
||||
# redirect_to :action=>'atom', :status=>302
|
||||
# redirect_to post_url(@post), :status => :found
|
||||
# redirect_to :action=>'atom', :status => :moved_permanently
|
||||
# redirect_to post_url(@post), :status => 301
|
||||
# redirect_to :action=>'atom', :status => 302
|
||||
#
|
||||
# When using <tt>redirect_to :back</tt>, if there is no referrer,
|
||||
# RedirectBackError will be raised. You may specify some fallback
|
||||
# The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
|
||||
# integer, or a symbol representing the downcased, underscored and symbolized description.
|
||||
#
|
||||
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names
|
||||
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
|
||||
#
|
||||
# Examples:
|
||||
# redirect_to post_url(@post), :alert => "Watch it, mister!"
|
||||
# redirect_to post_url(@post), :status=> :found, :notice => "Pay attention to the road"
|
||||
# redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id }
|
||||
# redirect_to { :action=>'atom' }, :alert => "Something serious happened"
|
||||
#
|
||||
# When using <tt>redirect_to :back</tt>, if there is no referrer, RedirectBackError will be raised. You may specify some fallback
|
||||
# behavior for this case by rescuing RedirectBackError.
|
||||
def redirect_to(options = {}, response_status = {}) #:doc:
|
||||
raise ActionControllerError.new("Cannot redirect to nil!") if options.nil?
|
||||
@@ -1403,7 +1414,7 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
Base.class_eval do
|
||||
[ Filters, Layout, Benchmarking, Rescue, Flash, MimeResponds, Helpers,
|
||||
[ Filters, Layout, Benchmarking, Rescue, MimeResponds, Helpers, Flash,
|
||||
Cookies, Caching, Verification, Streaming, SessionManagement,
|
||||
HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods,
|
||||
RecordIdentifier, RequestForgeryProtection, Translation
|
||||
|
||||
@@ -22,12 +22,13 @@ module ActionController #:nodoc:
|
||||
# ActionController::Base.cache_store = :file_store, "/path/to/cache/directory"
|
||||
# ActionController::Base.cache_store = :drb_store, "druby://localhost:9192"
|
||||
# ActionController::Base.cache_store = :mem_cache_store, "localhost"
|
||||
# ActionController::Base.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211")
|
||||
# ActionController::Base.cache_store = MyOwnStore.new("parameter")
|
||||
module Caching
|
||||
autoload :Actions, 'action_controller/caching/actions'
|
||||
autoload :Fragments, 'action_controller/caching/fragments'
|
||||
autoload :Pages, 'action_controller/caching/pages'
|
||||
autoload :Sweeper, 'action_controller/caching/sweeping'
|
||||
autoload :Sweeper, 'action_controller/caching/sweeper'
|
||||
autoload :Sweeping, 'action_controller/caching/sweeping'
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
|
||||
@@ -61,7 +61,9 @@ module ActionController #:nodoc:
|
||||
filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) }
|
||||
|
||||
cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options)
|
||||
around_filter(cache_filter, filter_options)
|
||||
around_filter(filter_options) do |controller, action|
|
||||
cache_filter.filter(controller, action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,6 +85,12 @@ module ActionController #:nodoc:
|
||||
@options = options
|
||||
end
|
||||
|
||||
def filter(controller, action)
|
||||
should_continue = before(controller)
|
||||
action.call if should_continue
|
||||
after(controller)
|
||||
end
|
||||
|
||||
def before(controller)
|
||||
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
|
||||
if cache = controller.read_fragment(cache_path.path, @options[:store_options])
|
||||
|
||||
@@ -37,7 +37,7 @@ module ActionController #:nodoc:
|
||||
def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc:
|
||||
if perform_caching
|
||||
if cache = read_fragment(name, options)
|
||||
buffer.concat(cache)
|
||||
buffer.safe_concat(cache.html_safe)
|
||||
else
|
||||
pos = buffer.length
|
||||
block.call
|
||||
@@ -52,9 +52,9 @@ module ActionController #:nodoc:
|
||||
def write_fragment(key, content, options = nil)
|
||||
return content unless cache_configured?
|
||||
|
||||
key = fragment_cache_key(key)
|
||||
|
||||
self.class.benchmark "Cached fragment miss: #{key}" do
|
||||
key = fragment_cache_key(key)
|
||||
content = content.html_safe.to_str if content.respond_to?(:html_safe)
|
||||
cache_store.write(key, content, options)
|
||||
end
|
||||
|
||||
@@ -66,9 +66,9 @@ module ActionController #:nodoc:
|
||||
return unless cache_configured?
|
||||
|
||||
key = fragment_cache_key(key)
|
||||
|
||||
self.class.benchmark "Cached fragment hit: #{key}" do
|
||||
cache_store.read(key, options)
|
||||
result = cache_store.read(key, options)
|
||||
result.respond_to?(:html_safe) ? result.html_safe : result
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
45
actionpack/lib/action_controller/caching/sweeper.rb
Normal file
45
actionpack/lib/action_controller/caching/sweeper.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
require 'active_record'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
module Caching
|
||||
class Sweeper < ActiveRecord::Observer #:nodoc:
|
||||
attr_accessor :controller
|
||||
|
||||
def before(controller)
|
||||
self.controller = controller
|
||||
callback(:before) if controller.perform_caching
|
||||
end
|
||||
|
||||
def after(controller)
|
||||
callback(:after) if controller.perform_caching
|
||||
# Clean up, so that the controller can be collected after this request
|
||||
self.controller = nil
|
||||
end
|
||||
|
||||
protected
|
||||
# gets the action cache path for the given options.
|
||||
def action_path_for(options)
|
||||
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
|
||||
end
|
||||
|
||||
# Retrieve instance variables set in the controller.
|
||||
def assigns(key)
|
||||
controller.instance_variable_get("@#{key}")
|
||||
end
|
||||
|
||||
private
|
||||
def callback(timing)
|
||||
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
|
||||
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
|
||||
|
||||
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
|
||||
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
return if @controller.nil?
|
||||
@controller.__send__(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -51,47 +51,5 @@ module ActionController #:nodoc:
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
|
||||
class Sweeper < ActiveRecord::Observer #:nodoc:
|
||||
attr_accessor :controller
|
||||
|
||||
def before(controller)
|
||||
self.controller = controller
|
||||
callback(:before) if controller.perform_caching
|
||||
end
|
||||
|
||||
def after(controller)
|
||||
callback(:after) if controller.perform_caching
|
||||
# Clean up, so that the controller can be collected after this request
|
||||
self.controller = nil
|
||||
end
|
||||
|
||||
protected
|
||||
# gets the action cache path for the given options.
|
||||
def action_path_for(options)
|
||||
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
|
||||
end
|
||||
|
||||
# Retrieve instance variables set in the controller.
|
||||
def assigns(key)
|
||||
controller.instance_variable_get("@#{key}")
|
||||
end
|
||||
|
||||
private
|
||||
def callback(timing)
|
||||
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
|
||||
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
|
||||
|
||||
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
|
||||
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
return if @controller.nil?
|
||||
@controller.__send__(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,18 +46,21 @@ module ActionController #:nodoc:
|
||||
module Cookies
|
||||
def self.included(base)
|
||||
base.helper_method :cookies
|
||||
base.cattr_accessor :cookie_verifier_secret
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns the cookie container, which operates as described above.
|
||||
def cookies
|
||||
CookieJar.new(self)
|
||||
@cookies ||= CookieJar.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CookieJar < Hash #:nodoc:
|
||||
attr_reader :controller
|
||||
|
||||
def initialize(controller)
|
||||
@controller, @cookies = controller, controller.request.cookies
|
||||
@controller, @cookies, @secure = controller, controller.request.cookies, controller.request.ssl?
|
||||
super()
|
||||
update(@cookies)
|
||||
end
|
||||
@@ -78,7 +81,7 @@ module ActionController #:nodoc:
|
||||
|
||||
options[:path] = "/" unless options.has_key?(:path)
|
||||
super(key.to_s, options[:value])
|
||||
@controller.response.set_cookie(key, options)
|
||||
@controller.response.set_cookie(key, options) if write_cookie?(options)
|
||||
end
|
||||
|
||||
# Removes the cookie on the client machine by setting the value to an empty string
|
||||
@@ -87,8 +90,108 @@ module ActionController #:nodoc:
|
||||
def delete(key, options = {})
|
||||
options.symbolize_keys!
|
||||
options[:path] = "/" unless options.has_key?(:path)
|
||||
super(key.to_s)
|
||||
value = super(key.to_s)
|
||||
@controller.response.delete_cookie(key, options)
|
||||
value
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
|
||||
#
|
||||
# cookies.permanent[:prefers_open_id] = true
|
||||
# # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
#
|
||||
# This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
|
||||
#
|
||||
# This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
|
||||
#
|
||||
# cookies.permanent.signed[:remember_me] = current_user.id
|
||||
# # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
|
||||
def permanent
|
||||
@permanent ||= PermanentCookieJar.new(self)
|
||||
end
|
||||
|
||||
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
|
||||
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
|
||||
# cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
|
||||
# be raised.
|
||||
#
|
||||
# This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# cookies.signed[:discount] = 45
|
||||
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
|
||||
#
|
||||
# cookies.signed[:discount] # => 45
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_cookie?(cookie)
|
||||
@secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development?
|
||||
end
|
||||
end
|
||||
|
||||
class PermanentCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar)
|
||||
@parent_jar = parent_jar
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
else
|
||||
options = { :value => options }
|
||||
end
|
||||
|
||||
options[:expires] = 20.years.from_now
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
|
||||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
|
||||
def controller
|
||||
@parent_jar.controller
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
|
||||
class SignedCookieJar < CookieJar #:nodoc:
|
||||
def initialize(parent_jar)
|
||||
unless parent_jar.controller.class.cookie_verifier_secret
|
||||
raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies"
|
||||
end
|
||||
|
||||
@parent_jar = parent_jar
|
||||
@verifier = ActiveSupport::MessageVerifier.new(@parent_jar.controller.class.cookie_verifier_secret)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
if value = @parent_jar[name]
|
||||
@verifier.verify(value)
|
||||
end
|
||||
end
|
||||
|
||||
def []=(key, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
options[:value] = @verifier.generate(options[:value])
|
||||
else
|
||||
options = { :value => @verifier.generate(options) }
|
||||
end
|
||||
|
||||
@parent_jar[key] = options
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
@parent_jar.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,13 +2,12 @@ module ActionController
|
||||
# Dispatches requests to the appropriate controller and takes care of
|
||||
# reloading the app after each request when Dependencies.load? is true.
|
||||
class Dispatcher
|
||||
@@cache_classes = true
|
||||
|
||||
class << self
|
||||
def define_dispatcher_callbacks(cache_classes)
|
||||
@@cache_classes = cache_classes
|
||||
unless cache_classes
|
||||
unless self.middleware.include?(Reloader)
|
||||
self.middleware.insert_after(Failsafe, Reloader)
|
||||
end
|
||||
|
||||
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
|
||||
end
|
||||
|
||||
@@ -79,7 +78,7 @@ module ActionController
|
||||
# DEPRECATE: Remove arguments, since they are only used by CGI
|
||||
def initialize(output = $stdout, request = nil, response = nil)
|
||||
@output = output
|
||||
@app = @@middleware.build(lambda { |env| self.dup._call(env) })
|
||||
build_middleware_stack if @@cache_classes
|
||||
end
|
||||
|
||||
def dispatch
|
||||
@@ -103,7 +102,18 @@ module ActionController
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
if @@cache_classes
|
||||
@app.call(env)
|
||||
else
|
||||
Reloader.run do
|
||||
# When class reloading is turned on, we will want to rebuild the
|
||||
# middleware stack every time we process a request. If we don't
|
||||
# rebuild the middleware stack, then the stack may contain references
|
||||
# to old classes metal classes, which will b0rk class reloading.
|
||||
build_middleware_stack
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def _call(env)
|
||||
@@ -114,5 +124,10 @@ module ActionController
|
||||
def flush_logger
|
||||
Base.logger.flush
|
||||
end
|
||||
|
||||
private
|
||||
def build_middleware_stack
|
||||
@app = @@middleware.build(lambda { |env| self.dup._call(env) })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
require 'erb'
|
||||
|
||||
module ActionController
|
||||
# The Failsafe middleware is usually the top-most middleware in the Rack
|
||||
# middleware chain. It returns the underlying middleware's response, but if
|
||||
# the underlying middle raises an exception then Failsafe will log the
|
||||
# exception into the Rails log file, and will attempt to return an error
|
||||
# message response.
|
||||
#
|
||||
# Failsafe is a last resort for logging errors and for telling the HTTP
|
||||
# client that something went wrong. Do not confuse this with the
|
||||
# ActionController::Rescue module, which is responsible for catching
|
||||
# exceptions at deeper levels. Unlike Failsafe, which is as simple as
|
||||
# possible, Rescue provides features that allow developers to hook into
|
||||
# the error handling logic, and can customize the error message response
|
||||
# based on the HTTP client's IP.
|
||||
class Failsafe
|
||||
cattr_accessor :error_file_path
|
||||
self.error_file_path = Rails.public_path if defined?(Rails.public_path)
|
||||
@@ -11,7 +26,7 @@ module ActionController
|
||||
@app.call(env)
|
||||
rescue Exception => exception
|
||||
# Reraise exception in test environment
|
||||
if env["rack.test"]
|
||||
if defined?(Rails) && Rails.env.test?
|
||||
raise exception
|
||||
else
|
||||
failsafe_response(exception)
|
||||
@@ -21,24 +36,44 @@ module ActionController
|
||||
private
|
||||
def failsafe_response(exception)
|
||||
log_failsafe_exception(exception)
|
||||
[500, {'Content-Type' => 'text/html'}, failsafe_response_body]
|
||||
[500, {'Content-Type' => 'text/html'}, [failsafe_response_body]]
|
||||
rescue Exception => failsafe_error # Logger or IO errors
|
||||
$stderr.puts "Error during failsafe response: #{failsafe_error}"
|
||||
end
|
||||
|
||||
def failsafe_response_body
|
||||
error_path = "#{self.class.error_file_path}/500.html"
|
||||
if File.exist?(error_path)
|
||||
File.read(error_path)
|
||||
error_template_path = "#{self.class.error_file_path}/500.html"
|
||||
if File.exist?(error_template_path)
|
||||
begin
|
||||
result = render_template(error_template_path)
|
||||
rescue Exception
|
||||
result = nil
|
||||
end
|
||||
else
|
||||
"<html><body><h1>500 Internal Server Error</h1></body></html>"
|
||||
result = nil
|
||||
end
|
||||
if result.nil?
|
||||
result = "<html><body><h1>500 Internal Server Error</h1>" <<
|
||||
"If you are the administrator of this website, then please read this web " <<
|
||||
"application's log file to find out what went wrong.</body></html>"
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# The default 500.html uses the h() method.
|
||||
def h(text) # :nodoc:
|
||||
ERB::Util.h(text)
|
||||
end
|
||||
|
||||
def render_template(filename)
|
||||
ERB.new(File.read(filename)).result(binding)
|
||||
end
|
||||
|
||||
def log_failsafe_exception(exception)
|
||||
message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: 500 Internal Server Error\n"
|
||||
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
|
||||
failsafe_logger.fatal(message)
|
||||
failsafe_logger.flush if failsafe_logger.respond_to?(:flush)
|
||||
end
|
||||
|
||||
def failsafe_logger
|
||||
|
||||
@@ -29,8 +29,13 @@ module ActionController #:nodoc:
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
include InstanceMethods
|
||||
|
||||
alias_method_chain :perform_action, :flash
|
||||
alias_method_chain :reset_session, :flash
|
||||
alias_method_chain :redirect_to, :flash
|
||||
|
||||
helper_method :alert
|
||||
helper_method :notice
|
||||
end
|
||||
end
|
||||
|
||||
@@ -120,6 +125,11 @@ module ActionController #:nodoc:
|
||||
(@used.keys - keys).each{ |k| @used.delete(k) }
|
||||
end
|
||||
|
||||
def store(session, key = "flash")
|
||||
return if self.empty?
|
||||
session[key] = self
|
||||
end
|
||||
|
||||
private
|
||||
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
|
||||
# use() # marks the entire flash as used
|
||||
@@ -139,7 +149,10 @@ module ActionController #:nodoc:
|
||||
protected
|
||||
def perform_action_with_flash
|
||||
perform_action_without_flash
|
||||
remove_instance_variable(:@_flash) if defined? @_flash
|
||||
if defined? @_flash
|
||||
@_flash.store(session)
|
||||
remove_instance_variable(:@_flash)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_session_with_flash
|
||||
@@ -147,17 +160,54 @@ module ActionController #:nodoc:
|
||||
remove_instance_variable(:@_flash) if defined? @_flash
|
||||
end
|
||||
|
||||
def redirect_to_with_flash(options = {}, response_status_and_flash = {}) #:doc:
|
||||
if alert = response_status_and_flash.delete(:alert)
|
||||
flash[:alert] = alert
|
||||
end
|
||||
|
||||
if notice = response_status_and_flash.delete(:notice)
|
||||
flash[:notice] = notice
|
||||
end
|
||||
|
||||
if other_flashes = response_status_and_flash.delete(:flash)
|
||||
flash.update(other_flashes)
|
||||
end
|
||||
|
||||
redirect_to_without_flash(options, response_status_and_flash)
|
||||
end
|
||||
|
||||
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
|
||||
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
|
||||
# to put a new one.
|
||||
def flash #:doc:
|
||||
unless defined? @_flash
|
||||
@_flash = session["flash"] ||= FlashHash.new
|
||||
if !defined?(@_flash)
|
||||
@_flash = session["flash"] || FlashHash.new
|
||||
@_flash.sweep
|
||||
end
|
||||
|
||||
@_flash
|
||||
end
|
||||
|
||||
|
||||
# Convenience accessor for flash[:alert]
|
||||
def alert
|
||||
flash[:alert]
|
||||
end
|
||||
|
||||
# Convenience accessor for flash[:alert]=
|
||||
def alert=(message)
|
||||
flash[:alert] = message
|
||||
end
|
||||
|
||||
# Convenience accessor for flash[:notice]
|
||||
def notice
|
||||
flash[:notice]
|
||||
end
|
||||
|
||||
# Convenience accessor for flash[:notice]=
|
||||
def notice=(message)
|
||||
flash[:notice] = message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -139,7 +139,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def decode_credentials(request)
|
||||
ActiveSupport::Base64.decode64(authorization(request).split.last || '')
|
||||
ActiveSupport::Base64.decode64(authorization(request).split(' ', 2).last || '')
|
||||
end
|
||||
|
||||
def encode_credentials(user_name, password)
|
||||
@@ -183,7 +183,7 @@ module ActionController
|
||||
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
|
||||
end
|
||||
|
||||
# Raises error unless the request credentials response value matches the expected value.
|
||||
# Returns false unless the request credentials response value matches the expected value.
|
||||
# First try the password as a ha1 digest password. If this fails, then try it as a plain
|
||||
# text password.
|
||||
def validate_digest_response(request, realm, &password_procedure)
|
||||
@@ -192,9 +192,13 @@ module ActionController
|
||||
|
||||
if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
|
||||
password = password_procedure.call(credentials[:username])
|
||||
return false unless password
|
||||
|
||||
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
|
||||
uri = credentials[:uri][0,1] == '/' ? request.request_uri : request.url
|
||||
|
||||
[true, false].any? do |password_is_ha1|
|
||||
expected = expected_response(request.env['REQUEST_METHOD'], request.env['REQUEST_URI'], credentials, password, password_is_ha1)
|
||||
expected = expected_response(method, uri, credentials, password, password_is_ha1)
|
||||
expected == credentials[:response]
|
||||
end
|
||||
end
|
||||
@@ -223,9 +227,9 @@ module ActionController
|
||||
end
|
||||
|
||||
def decode_credentials(header)
|
||||
header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
|
||||
header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}.with_indifferent_access) do |hash, pair|
|
||||
key, value = pair.split('=', 2)
|
||||
hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
|
||||
hash[key.strip] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
|
||||
hash
|
||||
end
|
||||
end
|
||||
@@ -285,6 +289,7 @@ module ActionController
|
||||
# allow a user to use new nonce without prompting user again for their
|
||||
# username and password.
|
||||
def validate_nonce(request, value, seconds_to_timeout=5*60)
|
||||
return false if value.nil?
|
||||
t = Base64.decode64(value).split(":").first.to_i
|
||||
nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require 'stringio'
|
||||
require 'uri'
|
||||
require 'active_support/test_case'
|
||||
require 'action_controller/rack_lint_patch'
|
||||
|
||||
module ActionController
|
||||
module Integration #:nodoc:
|
||||
@@ -268,7 +269,9 @@ module ActionController
|
||||
|
||||
env["QUERY_STRING"] ||= ""
|
||||
|
||||
data = data.is_a?(IO) ? data : StringIO.new(data || '')
|
||||
data ||= ''
|
||||
data.force_encoding(Encoding::ASCII_8BIT) if data.respond_to?(:force_encoding)
|
||||
data = data.is_a?(IO) ? data : StringIO.new(data)
|
||||
|
||||
env.update(
|
||||
"REQUEST_METHOD" => method.to_s.upcase,
|
||||
@@ -284,7 +287,6 @@ module ActionController
|
||||
"REMOTE_ADDR" => remote_addr,
|
||||
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
|
||||
"CONTENT_LENGTH" => data ? data.length.to_s : nil,
|
||||
"HTTP_COOKIE" => encode_cookies,
|
||||
"HTTP_ACCEPT" => accept,
|
||||
|
||||
"rack.version" => [0,1],
|
||||
@@ -292,11 +294,11 @@ module ActionController
|
||||
"rack.errors" => StringIO.new,
|
||||
"rack.multithread" => true,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
|
||||
"rack.test" => true
|
||||
"rack.run_once" => false
|
||||
)
|
||||
|
||||
env['HTTP_COOKIE'] = encode_cookies if cookies.any?
|
||||
|
||||
(headers || {}).each do |key, value|
|
||||
key = key.to_s.upcase.gsub(/-/, "_")
|
||||
key = "HTTP_#{key}" unless env.has_key?(key) || key =~ /^HTTP_/
|
||||
@@ -311,12 +313,7 @@ module ActionController
|
||||
|
||||
ActionController::Base.clear_last_instantiation!
|
||||
|
||||
app = @application
|
||||
# Rack::Lint doesn't accept String headers or bodies in Ruby 1.9
|
||||
unless RUBY_VERSION >= '1.9.0' && Rack.release <= '0.9.0'
|
||||
app = Rack::Lint.new(app)
|
||||
end
|
||||
|
||||
app = Rack::Lint.new(@application)
|
||||
status, headers, body = app.call(env)
|
||||
@request_count += 1
|
||||
|
||||
@@ -327,13 +324,15 @@ module ActionController
|
||||
|
||||
@headers = Rack::Utils::HeaderHash.new(headers)
|
||||
|
||||
(@headers['Set-Cookie'] || "").split("\n").each do |cookie|
|
||||
cookies = @headers['Set-Cookie']
|
||||
cookies = cookies.to_s.split("\n") unless cookies.is_a?(Array)
|
||||
cookies.each do |cookie|
|
||||
name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2]
|
||||
@cookies[name] = value
|
||||
end
|
||||
|
||||
@body = ""
|
||||
if body.is_a?(String)
|
||||
if body.respond_to?(:to_str)
|
||||
@body << body
|
||||
else
|
||||
body.each { |part| @body << part }
|
||||
@@ -357,6 +356,8 @@ module ActionController
|
||||
# used in integration tests.
|
||||
@response.extend(TestResponseBehavior)
|
||||
|
||||
body.close if body.respond_to?(:close)
|
||||
|
||||
return @status
|
||||
rescue MultiPartNeededException
|
||||
boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
|
||||
@@ -414,15 +415,25 @@ module ActionController
|
||||
end
|
||||
|
||||
def multipart_requestify(params, first=true)
|
||||
returning Hash.new do |p|
|
||||
Array.new.tap do |p|
|
||||
params.each do |key, value|
|
||||
k = first ? CGI.escape(key.to_s) : "[#{CGI.escape(key.to_s)}]"
|
||||
k = first ? key.to_s : "[#{key.to_s}]"
|
||||
if Hash === value
|
||||
multipart_requestify(value, false).each do |subkey, subvalue|
|
||||
p[k + subkey] = subvalue
|
||||
p << [k + subkey, subvalue]
|
||||
end
|
||||
elsif Array === value
|
||||
value.each do |element|
|
||||
if Hash === element || Array === element
|
||||
multipart_requestify(element, false).each do |subkey, subvalue|
|
||||
p << ["#{k}[]#{subkey}", subvalue]
|
||||
end
|
||||
else
|
||||
p << ["#{k}[]", element]
|
||||
end
|
||||
end
|
||||
else
|
||||
p[k] = value
|
||||
p << [k, value]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -453,6 +464,7 @@ EOF
|
||||
end
|
||||
end.join("")+"--#{boundary}--\r"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# A module used to extend ActionController::Base, so that integration tests
|
||||
@@ -483,6 +495,11 @@ EOF
|
||||
end
|
||||
|
||||
module Runner
|
||||
def initialize(*args)
|
||||
super
|
||||
@integration_session = nil
|
||||
end
|
||||
|
||||
# Reset the current session. This is useful for testing multiple sessions
|
||||
# in a single test case.
|
||||
def reset!
|
||||
@@ -495,7 +512,7 @@ EOF
|
||||
reset! unless @integration_session
|
||||
# reset the html_document variable, but only for new get/post calls
|
||||
@html_document = nil unless %w(cookies assigns).include?(method)
|
||||
returning @integration_session.__send__(method, *args) do
|
||||
@integration_session.__send__(method, *args).tap do
|
||||
copy_session_variables!
|
||||
end
|
||||
end
|
||||
@@ -519,7 +536,7 @@ EOF
|
||||
if self.class.respond_to?(:fixture_table_names)
|
||||
self.class.fixture_table_names.each do |table_name|
|
||||
name = table_name.tr(".", "_")
|
||||
next unless respond_to?(name)
|
||||
next unless respond_to?(name, true)
|
||||
extras.__send__(:define_method, name) { |*args|
|
||||
delegate.send(name, *args)
|
||||
}
|
||||
@@ -550,8 +567,12 @@ EOF
|
||||
# Delegate unhandled messages to the current session instance.
|
||||
def method_missing(sym, *args, &block)
|
||||
reset! unless @integration_session
|
||||
returning @integration_session.__send__(sym, *args, &block) do
|
||||
copy_session_variables!
|
||||
if @integration_session.respond_to?(sym)
|
||||
@integration_session.__send__(sym, *args, &block).tap do
|
||||
copy_session_variables!
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -194,6 +194,11 @@ module ActionController #:nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@real_format = nil
|
||||
end
|
||||
|
||||
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
|
||||
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
|
||||
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
|
||||
@@ -221,7 +226,7 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def find_layout(layout, format, html_fallback=false) #:nodoc:
|
||||
view_paths.find_template(layout.to_s =~ /layouts\// ? layout : "layouts/#{layout}", format, html_fallback)
|
||||
view_paths.find_template(layout.to_s =~ /\A\/|layouts\// ? layout : "layouts/#{layout}", format, html_fallback)
|
||||
rescue ActionView::MissingTemplate
|
||||
raise if Mime::Type.lookup_by_extension(format.to_s).html?
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ use "ActionController::Failsafe"
|
||||
use lambda { ActionController::Base.session_store },
|
||||
lambda { ActionController::Base.session_options }
|
||||
|
||||
use "ActionController::RewindableInput"
|
||||
use "ActionController::ParamsParser"
|
||||
use "Rack::MethodOverride"
|
||||
use "Rack::Head"
|
||||
|
||||
use "ActionController::StringCoercion"
|
||||
|
||||
@@ -47,6 +47,8 @@ module ActionController
|
||||
false
|
||||
end
|
||||
rescue Exception => e # YAML, XML or Ruby code block errors
|
||||
logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
|
||||
|
||||
raise
|
||||
{ "body" => request.raw_post,
|
||||
"content_type" => request.content_type,
|
||||
@@ -67,5 +69,9 @@ module ActionController
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def logger
|
||||
defined?(Rails.logger) ? Rails.logger : Logger.new($stderr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,8 +76,7 @@ module ActionController
|
||||
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
|
||||
end
|
||||
|
||||
record = extract_record(record_or_hash_or_array)
|
||||
namespace = extract_namespace(record_or_hash_or_array)
|
||||
record = extract_record(record_or_hash_or_array)
|
||||
|
||||
args = case record_or_hash_or_array
|
||||
when Hash; [ record_or_hash_or_array ]
|
||||
@@ -98,8 +97,7 @@ module ActionController
|
||||
end
|
||||
|
||||
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
|
||||
|
||||
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options)
|
||||
named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
|
||||
|
||||
url_options = options.except(:action, :routing_type)
|
||||
unless url_options.empty?
|
||||
@@ -117,7 +115,7 @@ module ActionController
|
||||
end
|
||||
|
||||
%w(edit new).each do |action|
|
||||
module_eval <<-EOT, __FILE__, __LINE__
|
||||
module_eval <<-EOT, __FILE__, __LINE__ + 1
|
||||
def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
|
||||
polymorphic_url( # polymorphic_url(
|
||||
record_or_hash, # record_or_hash,
|
||||
@@ -153,7 +151,7 @@ module ActionController
|
||||
options[:routing_type] || :url
|
||||
end
|
||||
|
||||
def build_named_route_call(records, namespace, inflection, options = {})
|
||||
def build_named_route_call(records, inflection, options = {})
|
||||
unless records.is_a?(Array)
|
||||
record = extract_record(records)
|
||||
route = ''
|
||||
@@ -163,7 +161,7 @@ module ActionController
|
||||
if parent.is_a?(Symbol) || parent.is_a?(String)
|
||||
string << "#{parent}_"
|
||||
else
|
||||
string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize
|
||||
string << RecordIdentifier.__send__("plural_class_name", parent).singularize
|
||||
string << "_"
|
||||
end
|
||||
end
|
||||
@@ -172,12 +170,12 @@ module ActionController
|
||||
if record.is_a?(Symbol) || record.is_a?(String)
|
||||
route << "#{record}_"
|
||||
else
|
||||
route << "#{RecordIdentifier.__send__("plural_class_name", record)}"
|
||||
route << RecordIdentifier.__send__("plural_class_name", record)
|
||||
route = route.singularize if inflection == :singular
|
||||
route << "_"
|
||||
end
|
||||
|
||||
action_prefix(options) + namespace + route + routing_type(options).to_s
|
||||
action_prefix(options) + route + routing_type(options).to_s
|
||||
end
|
||||
|
||||
def extract_record(record_or_hash_or_array)
|
||||
@@ -187,18 +185,5 @@ module ActionController
|
||||
else record_or_hash_or_array
|
||||
end
|
||||
end
|
||||
|
||||
# Remove the first symbols from the array and return the url prefix
|
||||
# implied by those symbols.
|
||||
def extract_namespace(record_or_hash_or_array)
|
||||
return "" unless record_or_hash_or_array.is_a?(Array)
|
||||
|
||||
namespace_keys = []
|
||||
while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol)
|
||||
namespace_keys << record_or_hash_or_array.shift
|
||||
end
|
||||
|
||||
namespace_keys.map {|k| "#{k}_"}.join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
36
actionpack/lib/action_controller/rack_lint_patch.rb
Normal file
36
actionpack/lib/action_controller/rack_lint_patch.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# Rack 1.0 does not allow string subclass body. This does not play well with our ActiveSupport::SafeBuffer.
|
||||
# The next release of Rack will be allowing string subclass body - http://github.com/rack/rack/commit/de668df02802a0335376a81ba709270e43ba9d55
|
||||
# TODO : Remove this monkey patch after the next release of Rack
|
||||
|
||||
module RackLintPatch
|
||||
module AllowStringSubclass
|
||||
def self.included(base)
|
||||
base.send :alias_method, :each, :each_with_hack
|
||||
end
|
||||
|
||||
def each_with_hack
|
||||
@closed = false
|
||||
|
||||
@body.each { |part|
|
||||
assert("Body yielded non-string value #{part.inspect}") {
|
||||
part.kind_of?(String)
|
||||
}
|
||||
yield part
|
||||
}
|
||||
|
||||
if @body.respond_to?(:to_path)
|
||||
assert("The file identified by body.to_path does not exist") {
|
||||
::File.exist? @body.to_path
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
app = proc {|env| [200, {"Content-Type" => "text/plain", "Content-Length" => "12"}, [Class.new(String).new("Hello World!")]] }
|
||||
response = Rack::MockRequest.new(Rack::Lint.new(app)).get('/')
|
||||
rescue Rack::Lint::LintError => e
|
||||
raise(e) unless e.message =~ /Body yielded non-string value/
|
||||
Rack::Lint.send :include, AllowStringSubclass
|
||||
end
|
||||
end
|
||||
@@ -1,14 +1,54 @@
|
||||
require 'thread'
|
||||
|
||||
module ActionController
|
||||
class Reloader
|
||||
def initialize(app)
|
||||
@app = app
|
||||
@@default_lock = Mutex.new
|
||||
cattr_accessor :default_lock
|
||||
|
||||
class BodyWrapper
|
||||
def initialize(body, lock)
|
||||
@body = body
|
||||
@lock = lock
|
||||
end
|
||||
|
||||
def close
|
||||
@body.close if @body.respond_to?(:close)
|
||||
ensure
|
||||
Dispatcher.cleanup_application
|
||||
@lock.unlock
|
||||
end
|
||||
|
||||
def method_missing(*args, &block)
|
||||
@body.send(*args, &block)
|
||||
end
|
||||
|
||||
def respond_to?(symbol, include_private = false)
|
||||
symbol == :close || @body.respond_to?(symbol, include_private)
|
||||
end
|
||||
end
|
||||
|
||||
def call(env)
|
||||
Dispatcher.reload_application
|
||||
@app.call(env)
|
||||
ensure
|
||||
Dispatcher.cleanup_application
|
||||
def self.run(lock = @@default_lock)
|
||||
lock.lock
|
||||
begin
|
||||
Dispatcher.reload_application
|
||||
status, headers, body = yield
|
||||
# We do not want to call 'cleanup_application' in an ensure block
|
||||
# because the returned Rack response body may lazily generate its data. This
|
||||
# is for example the case if one calls
|
||||
#
|
||||
# render :text => lambda { ... code here which refers to application models ... }
|
||||
#
|
||||
# in an ActionController.
|
||||
#
|
||||
# Instead, we will want to cleanup the application code after the request is
|
||||
# completely finished. So we wrap the body in a BodyWrapper class so that
|
||||
# when the Rack handler calls #close during the end of the request, we get to
|
||||
# run our cleanup code.
|
||||
[status, headers, BodyWrapper.new(body, lock)]
|
||||
rescue Exception
|
||||
lock.unlock
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -95,6 +95,10 @@ module ActionController
|
||||
end
|
||||
end
|
||||
|
||||
def media_type
|
||||
content_type.to_s
|
||||
end
|
||||
|
||||
# Returns the accepted MIME type for the request.
|
||||
def accepts
|
||||
@accepts ||= begin
|
||||
@@ -383,7 +387,7 @@ EOM
|
||||
alias_method :params, :parameters
|
||||
|
||||
def path_parameters=(parameters) #:nodoc:
|
||||
@env["rack.routing_args"] = parameters
|
||||
@env["action_controller.request.path_parameters"] = parameters
|
||||
@symbolized_path_parameters = @parameters = nil
|
||||
end
|
||||
|
||||
@@ -399,7 +403,7 @@ EOM
|
||||
#
|
||||
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
|
||||
def path_parameters
|
||||
@env["rack.routing_args"] ||= {}
|
||||
@env["action_controller.request.path_parameters"] ||= {}
|
||||
end
|
||||
|
||||
# The request body is an IO input stream. If the RAW_POST_DATA environment
|
||||
@@ -442,8 +446,10 @@ EOM
|
||||
end
|
||||
|
||||
def reset_session
|
||||
@env['rack.session.options'].delete(:id)
|
||||
@env['rack.session'] = {}
|
||||
# session may be a hash, if so, we do not want to call destroy
|
||||
# fixes issue 6440
|
||||
session.destroy if session and session.respond_to?(:destroy)
|
||||
self.session = {}
|
||||
end
|
||||
|
||||
def session_options
|
||||
|
||||
@@ -76,21 +76,29 @@ module ActionController #:nodoc:
|
||||
protected
|
||||
# The actual before_filter that is used. Modify this to change how you handle unverified requests.
|
||||
def verify_authenticity_token
|
||||
verified_request? || raise(ActionController::InvalidAuthenticityToken)
|
||||
verified_request? || handle_unverified_request
|
||||
end
|
||||
|
||||
def handle_unverified_request
|
||||
reset_session
|
||||
end
|
||||
|
||||
# Returns true or false if a request is verified. Checks:
|
||||
#
|
||||
# * is the format restricted? By default, only HTML and AJAX requests are checked.
|
||||
# * is the format restricted? By default, only HTML requests are checked.
|
||||
# * is it a GET request? Gets should be safe and idempotent
|
||||
# * Does the form_authenticity_token match the given token value from the params?
|
||||
def verified_request?
|
||||
!protect_against_forgery? ||
|
||||
request.method == :get ||
|
||||
!verifiable_request_format? ||
|
||||
form_authenticity_token == params[request_forgery_protection_token]
|
||||
!protect_against_forgery? ||
|
||||
request.get? ||
|
||||
form_authenticity_token == form_authenticity_param ||
|
||||
form_authenticity_token == request.headers['X-CSRF-Token']
|
||||
end
|
||||
|
||||
|
||||
def form_authenticity_param
|
||||
params[request_forgery_protection_token]
|
||||
end
|
||||
|
||||
def verifiable_request_format?
|
||||
!request.content_type.nil? && request.content_type.verify_request?
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ module ActionController #:nodoc:
|
||||
# behavior is achieved by overriding the <tt>rescue_action_in_public</tt>
|
||||
# and <tt>rescue_action_locally</tt> methods.
|
||||
module Rescue
|
||||
LOCALHOST = '127.0.0.1'.freeze
|
||||
LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/].freeze
|
||||
|
||||
DEFAULT_RESCUE_RESPONSE = :internal_server_error
|
||||
DEFAULT_RESCUE_RESPONSES = {
|
||||
@@ -122,7 +122,7 @@ module ActionController #:nodoc:
|
||||
# method if you wish to redefine the meaning of a local request to
|
||||
# include remote IP addresses or other criteria.
|
||||
def local_request? #:doc:
|
||||
request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST
|
||||
LOCALHOST.any?{ |local_ip| request.remote_addr =~ local_ip && request.remote_ip =~ local_ip }
|
||||
end
|
||||
|
||||
# Render detailed diagnostics for unhandled exceptions rescued from
|
||||
|
||||
@@ -317,9 +317,10 @@ module ActionController
|
||||
# notes.resources :attachments
|
||||
# end
|
||||
#
|
||||
# * <tt>:path_names</tt> - Specify different names for the 'new' and 'edit' actions. For example:
|
||||
# * <tt>:path_names</tt> - Specify different path names for the actions. For example:
|
||||
# # new_products_path == '/productos/nuevo'
|
||||
# map.resources :products, :as => 'productos', :path_names => { :new => 'nuevo', :edit => 'editar' }
|
||||
# # bids_product_path(1) == '/productos/1/licitacoes'
|
||||
# map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' }
|
||||
#
|
||||
# You can also set default action names from an environment, like this:
|
||||
# config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' }
|
||||
@@ -525,16 +526,16 @@ module ActionController
|
||||
resource = Resource.new(entities, options)
|
||||
|
||||
with_options :controller => resource.controller do |map|
|
||||
map_collection_actions(map, resource)
|
||||
map_default_collection_actions(map, resource)
|
||||
map_new_actions(map, resource)
|
||||
map_member_actions(map, resource)
|
||||
|
||||
map_associations(resource, options)
|
||||
|
||||
if block_given?
|
||||
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
|
||||
end
|
||||
|
||||
map_collection_actions(map, resource)
|
||||
map_default_collection_actions(map, resource)
|
||||
map_new_actions(map, resource)
|
||||
map_member_actions(map, resource)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -542,16 +543,16 @@ module ActionController
|
||||
resource = SingletonResource.new(entities, options)
|
||||
|
||||
with_options :controller => resource.controller do |map|
|
||||
map_collection_actions(map, resource)
|
||||
map_new_actions(map, resource)
|
||||
map_member_actions(map, resource)
|
||||
map_default_singleton_actions(map, resource)
|
||||
|
||||
map_associations(resource, options)
|
||||
|
||||
if block_given?
|
||||
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
|
||||
end
|
||||
|
||||
map_collection_actions(map, resource)
|
||||
map_new_actions(map, resource)
|
||||
map_member_actions(map, resource)
|
||||
map_default_singleton_actions(map, resource)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -586,7 +587,10 @@ module ActionController
|
||||
resource.collection_methods.each do |method, actions|
|
||||
actions.each do |action|
|
||||
[method].flatten.each do |m|
|
||||
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
|
||||
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
|
||||
action_path ||= action
|
||||
|
||||
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -655,7 +659,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def add_conditions_for(conditions, method)
|
||||
returning({:conditions => conditions.dup}) do |options|
|
||||
({:conditions => conditions.dup}).tap do |options|
|
||||
options[:conditions][:method] = method unless method == :any
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,8 @@ module ActionController # :nodoc:
|
||||
@block = nil
|
||||
|
||||
@body = "",
|
||||
@session, @assigns = [], []
|
||||
@session = []
|
||||
@assigns = []
|
||||
end
|
||||
|
||||
def location; headers['Location'] end
|
||||
@@ -63,12 +64,13 @@ module ActionController # :nodoc:
|
||||
# the character set information will also be included in the content type
|
||||
# information.
|
||||
def content_type=(mime_type)
|
||||
self.headers["Content-Type"] =
|
||||
new_content_type =
|
||||
if mime_type =~ /charset/ || (c = charset).nil?
|
||||
mime_type.to_s
|
||||
else
|
||||
"#{mime_type}; charset=#{c}"
|
||||
end
|
||||
self.headers["Content-Type"] = URI.escape(new_content_type, "\r\n")
|
||||
end
|
||||
|
||||
# Returns the response's content MIME type, or nil if content type has been set.
|
||||
@@ -116,11 +118,7 @@ module ActionController # :nodoc:
|
||||
end
|
||||
|
||||
def etag=(etag)
|
||||
if etag.blank?
|
||||
headers.delete('ETag')
|
||||
else
|
||||
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
|
||||
end
|
||||
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
|
||||
end
|
||||
|
||||
def redirect(url, status)
|
||||
@@ -151,8 +149,8 @@ module ActionController # :nodoc:
|
||||
if @body.respond_to?(:call)
|
||||
@writer = lambda { |x| callback.call(x) }
|
||||
@body.call(self, self)
|
||||
elsif @body.is_a?(String)
|
||||
@body.each_line(&callback)
|
||||
elsif @body.respond_to?(:to_str)
|
||||
yield @body
|
||||
else
|
||||
@body.each(&callback)
|
||||
end
|
||||
@@ -166,6 +164,12 @@ module ActionController # :nodoc:
|
||||
str
|
||||
end
|
||||
|
||||
def flush #:nodoc:
|
||||
ActiveSupport::Deprecation.warn(
|
||||
'Calling output.flush is no longer needed for streaming output ' +
|
||||
'because ActionController::Response automatically handles it', caller)
|
||||
end
|
||||
|
||||
def set_cookie(key, value)
|
||||
if value.has_key?(:http_only)
|
||||
ActiveSupport::Deprecation.warn(
|
||||
@@ -195,7 +199,7 @@ module ActionController # :nodoc:
|
||||
|
||||
def nonempty_ok_response?
|
||||
ok = !status || status.to_s[0..2] == '200'
|
||||
ok && body.is_a?(String) && !body.empty?
|
||||
ok && body.is_a?(String) && !body.blank?
|
||||
end
|
||||
|
||||
def set_conditional_cache_control!
|
||||
@@ -226,7 +230,8 @@ module ActionController # :nodoc:
|
||||
end
|
||||
|
||||
def convert_cookies!
|
||||
headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact
|
||||
cookies = Array(headers['Set-Cookie']).compact
|
||||
headers['Set-Cookie'] = cookies unless cookies.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
module ActionController
|
||||
class RewindableInput
|
||||
class RewindableIO < ActiveSupport::BasicObject
|
||||
def initialize(io)
|
||||
@io = io
|
||||
@rewindable = io.is_a?(::StringIO)
|
||||
end
|
||||
|
||||
def method_missing(method, *args, &block)
|
||||
unless @rewindable
|
||||
@io = ::StringIO.new(@io.read)
|
||||
@rewindable = true
|
||||
end
|
||||
|
||||
@io.__send__(method, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env['rack.input'] = RewindableIO.new(env['rack.input'])
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -271,6 +271,9 @@ module ActionController
|
||||
|
||||
ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set
|
||||
|
||||
mattr_accessor :generate_best_match
|
||||
self.generate_best_match = true
|
||||
|
||||
# The root paths which may contain controller files
|
||||
mattr_accessor :controller_paths
|
||||
self.controller_paths = []
|
||||
@@ -374,7 +377,7 @@ module ActionController
|
||||
ActiveSupport::Inflector.module_eval do
|
||||
# Ensures that routes are reloaded when Rails inflections are updated.
|
||||
def inflections_with_route_reloading(&block)
|
||||
returning(inflections_without_route_reloading(&block)) {
|
||||
(inflections_without_route_reloading(&block)).tap {
|
||||
ActionController::Routing::Routes.reload! if block_given?
|
||||
}
|
||||
end
|
||||
|
||||
@@ -65,7 +65,7 @@ module ActionController
|
||||
# map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
|
||||
#
|
||||
def parameter_shell
|
||||
@parameter_shell ||= returning({}) do |shell|
|
||||
@parameter_shell ||= {}.tap do |shell|
|
||||
requirements.each do |key, requirement|
|
||||
shell[key] = requirement unless requirement.is_a? Regexp
|
||||
end
|
||||
@@ -76,7 +76,7 @@ module ActionController
|
||||
# includes keys that appear inside the path, and keys that have requirements
|
||||
# placed upon them.
|
||||
def significant_keys
|
||||
@significant_keys ||= returning([]) do |sk|
|
||||
@significant_keys ||= [].tap do |sk|
|
||||
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
|
||||
sk.concat requirements.keys
|
||||
sk.uniq!
|
||||
@@ -86,7 +86,7 @@ module ActionController
|
||||
# Return a hash of key/value pairs representing the keys in the route that
|
||||
# have defaults, or which are specified by non-regexp requirements.
|
||||
def defaults
|
||||
@defaults ||= returning({}) do |hash|
|
||||
@defaults ||= {}.tap do |hash|
|
||||
segments.each do |segment|
|
||||
next unless segment.respond_to? :default
|
||||
hash[segment.key] = segment.default unless segment.default.nil?
|
||||
|
||||
@@ -174,6 +174,7 @@ module ActionController
|
||||
#
|
||||
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
|
||||
def #{selector}(*args) # def users_url(*args)
|
||||
args.compact! #
|
||||
#
|
||||
#{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
|
||||
#
|
||||
@@ -305,6 +306,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def add_route(path, options = {})
|
||||
options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) }
|
||||
route = builder.build(path, options)
|
||||
routes << route
|
||||
route
|
||||
@@ -404,11 +406,14 @@ module ActionController
|
||||
end
|
||||
|
||||
# don't use the recalled keys when determining which routes to check
|
||||
routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
|
||||
future_routes, deprecated_routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
|
||||
routes = Routing.generate_best_match ? deprecated_routes : future_routes
|
||||
|
||||
routes.each do |route|
|
||||
routes.each_with_index do |route, index|
|
||||
results = route.__send__(method, options, merged, expire_on)
|
||||
return results if results && (!results.is_a?(Array) || results.first)
|
||||
if results && (!results.is_a?(Array) || results.first)
|
||||
return results
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -436,7 +441,7 @@ module ActionController
|
||||
def recognize(request)
|
||||
params = recognize_path(request.path, extract_request_environment(request))
|
||||
request.path_parameters = params.with_indifferent_access
|
||||
"#{params[:controller].camelize}Controller".constantize
|
||||
"#{params[:controller].to_s.camelize}Controller".constantize
|
||||
end
|
||||
|
||||
def recognize_path(path, environment={})
|
||||
@@ -447,7 +452,10 @@ module ActionController
|
||||
@routes_by_controller ||= Hash.new do |controller_hash, controller|
|
||||
controller_hash[controller] = Hash.new do |action_hash, action|
|
||||
action_hash[action] = Hash.new do |key_hash, keys|
|
||||
key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
|
||||
key_hash[keys] = [
|
||||
routes_for_controller_and_action_and_keys(controller, action, keys),
|
||||
deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -459,10 +467,11 @@ module ActionController
|
||||
merged = options if expire_on[:controller]
|
||||
action = merged[:action] || 'index'
|
||||
|
||||
routes_by_controller[controller][action][merged.keys]
|
||||
routes_by_controller[controller][action][merged.keys][1]
|
||||
end
|
||||
|
||||
def routes_for_controller_and_action(controller, action)
|
||||
ActiveSupport::Deprecation.warn "routes_for_controller_and_action() has been deprecated. Please use routes_for()"
|
||||
selected = routes.select do |route|
|
||||
route.matches_controller_and_action? controller, action
|
||||
end
|
||||
@@ -470,6 +479,12 @@ module ActionController
|
||||
end
|
||||
|
||||
def routes_for_controller_and_action_and_keys(controller, action, keys)
|
||||
routes.select do |route|
|
||||
route.matches_controller_and_action? controller, action
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
|
||||
selected = routes.select do |route|
|
||||
route.matches_controller_and_action? controller, action
|
||||
end
|
||||
|
||||
@@ -2,13 +2,42 @@ require 'rack/utils'
|
||||
|
||||
module ActionController
|
||||
module Session
|
||||
class AbstractStore
|
||||
class AbstractStore
|
||||
ENV_SESSION_KEY = 'rack.session'.freeze
|
||||
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
|
||||
|
||||
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
|
||||
SET_COOKIE = 'Set-Cookie'.freeze
|
||||
|
||||
# thin wrapper around Hash that allows us to lazily
|
||||
# load session id into session_options
|
||||
class OptionsHash < Hash
|
||||
def initialize(by, env, default_options)
|
||||
@by = by
|
||||
@env = env
|
||||
@session_id_loaded = false
|
||||
merge!(default_options)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if key == :id
|
||||
load_session_id! unless super(:id) || has_session_id?
|
||||
end
|
||||
super(key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def has_session_id?
|
||||
@session_id_loaded
|
||||
end
|
||||
|
||||
def load_session_id!
|
||||
self[:id] = @by.send(:extract_session_id, @env)
|
||||
@session_id_loaded = true
|
||||
end
|
||||
end
|
||||
|
||||
class SessionHash < Hash
|
||||
def initialize(by, env)
|
||||
super()
|
||||
@@ -25,21 +54,42 @@ module ActionController
|
||||
end
|
||||
|
||||
def [](key)
|
||||
load! unless @loaded
|
||||
load_for_read!
|
||||
super
|
||||
end
|
||||
|
||||
def has_key?(key)
|
||||
load_for_read!
|
||||
super
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
load! unless @loaded
|
||||
load_for_write!
|
||||
super
|
||||
end
|
||||
|
||||
def clear
|
||||
load_for_write!
|
||||
super
|
||||
end
|
||||
|
||||
def to_hash
|
||||
load_for_read!
|
||||
h = {}.replace(self)
|
||||
h.delete_if { |k,v| v.nil? }
|
||||
h
|
||||
end
|
||||
|
||||
def update(hash)
|
||||
load_for_write!
|
||||
super
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
load_for_write!
|
||||
super
|
||||
end
|
||||
|
||||
def data
|
||||
ActiveSupport::Deprecation.warn(
|
||||
"ActionController::Session::AbstractStore::SessionHash#data " +
|
||||
@@ -48,40 +98,43 @@ module ActionController
|
||||
end
|
||||
|
||||
def inspect
|
||||
load! unless @loaded
|
||||
load_for_read!
|
||||
super
|
||||
end
|
||||
|
||||
def exists?
|
||||
return @exists if instance_variable_defined?(:@exists)
|
||||
@exists = @by.send(:exists?, @env)
|
||||
end
|
||||
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
def destroy
|
||||
clear
|
||||
@by.send(:destroy, @env) if @by
|
||||
@env[ENV_SESSION_OPTIONS_KEY][:id] = nil if @env && @env[ENV_SESSION_OPTIONS_KEY]
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
private
|
||||
def loaded?
|
||||
@loaded
|
||||
|
||||
def load_for_read!
|
||||
load! if !loaded? && exists?
|
||||
end
|
||||
|
||||
def load_for_write!
|
||||
load! unless loaded?
|
||||
end
|
||||
|
||||
def load!
|
||||
stale_session_check! do
|
||||
id, session = @by.send(:load_session, @env)
|
||||
(@env[ENV_SESSION_OPTIONS_KEY] ||= {})[:id] = id
|
||||
replace(session)
|
||||
@loaded = true
|
||||
end
|
||||
id, session = @by.send(:load_session, @env)
|
||||
@env[ENV_SESSION_OPTIONS_KEY][:id] = id
|
||||
replace(session)
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
def stale_session_check!
|
||||
yield
|
||||
rescue ArgumentError => argument_error
|
||||
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
|
||||
begin
|
||||
# Note that the regexp does not allow $1 to end with a ':'
|
||||
$1.constantize
|
||||
rescue LoadError, NameError => const_error
|
||||
raise ActionController::SessionRestoreError, "Session contains objects whose class definition isn\\'t available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: \#{const_error.message} [\#{const_error.class}])\n"
|
||||
end
|
||||
|
||||
retry
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
@@ -98,12 +151,18 @@ module ActionController
|
||||
# Process legacy CGI options
|
||||
options = options.symbolize_keys
|
||||
if options.has_key?(:session_path)
|
||||
ActiveSupport::Deprecation.warn "Giving :session_path to SessionStore is deprecated, " <<
|
||||
"please use :path instead", caller
|
||||
options[:path] = options.delete(:session_path)
|
||||
end
|
||||
if options.has_key?(:session_key)
|
||||
ActiveSupport::Deprecation.warn "Giving :session_key to SessionStore is deprecated, " <<
|
||||
"please use :key instead", caller
|
||||
options[:key] = options.delete(:session_key)
|
||||
end
|
||||
if options.has_key?(:session_http_only)
|
||||
ActiveSupport::Deprecation.warn "Giving :session_http_only to SessionStore is deprecated, " <<
|
||||
"please use :httponly instead", caller
|
||||
options[:httponly] = options.delete(:session_http_only)
|
||||
end
|
||||
|
||||
@@ -114,18 +173,18 @@ module ActionController
|
||||
end
|
||||
|
||||
def call(env)
|
||||
session = SessionHash.new(self, env)
|
||||
|
||||
env[ENV_SESSION_KEY] = session
|
||||
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
|
||||
|
||||
prepare!(env)
|
||||
response = @app.call(env)
|
||||
|
||||
session_data = env[ENV_SESSION_KEY]
|
||||
options = env[ENV_SESSION_OPTIONS_KEY]
|
||||
|
||||
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
|
||||
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after]
|
||||
request = ActionController::Request.new(env)
|
||||
|
||||
return response if (options[:secure] && !request.ssl?)
|
||||
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
|
||||
|
||||
sid = options[:id] || generate_sid
|
||||
|
||||
@@ -133,21 +192,12 @@ module ActionController
|
||||
return response
|
||||
end
|
||||
|
||||
cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid)
|
||||
cookie << "; domain=#{options[:domain]}" if options[:domain]
|
||||
cookie << "; path=#{options[:path]}" if options[:path]
|
||||
if options[:expire_after]
|
||||
expiry = Time.now + options[:expire_after]
|
||||
cookie << "; expires=#{expiry.httpdate}"
|
||||
end
|
||||
cookie << "; Secure" if options[:secure]
|
||||
cookie << "; HttpOnly" if options[:httponly]
|
||||
request_cookies = env["rack.request.cookie_hash"]
|
||||
|
||||
headers = response[1]
|
||||
unless headers[SET_COOKIE].blank?
|
||||
headers[SET_COOKIE] << "\n#{cookie}"
|
||||
else
|
||||
headers[SET_COOKIE] = cookie
|
||||
if (request_cookies.nil? || request_cookies[@key] != sid) || options[:expire_after]
|
||||
cookie = {:value => sid}
|
||||
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
|
||||
Rack::Utils.set_cookie_header!(response[1], @key, cookie.merge(options))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -155,18 +205,39 @@ module ActionController
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare!(env)
|
||||
env[ENV_SESSION_KEY] = SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
request = Rack::Request.new(env)
|
||||
sid = request.cookies[@key]
|
||||
unless @cookie_only
|
||||
sid ||= request.params[@key]
|
||||
stale_session_check! do
|
||||
sid = current_session_id(env)
|
||||
sid, session = get_session(env, sid)
|
||||
[sid, session]
|
||||
end
|
||||
sid, session = get_session(env, sid)
|
||||
[sid, session]
|
||||
end
|
||||
|
||||
def extract_session_id(env)
|
||||
stale_session_check! do
|
||||
request = Rack::Request.new(env)
|
||||
sid = request.cookies[@key]
|
||||
sid ||= request.params[@key] unless @cookie_only
|
||||
sid
|
||||
end
|
||||
end
|
||||
|
||||
def current_session_id(env)
|
||||
env[ENV_SESSION_OPTIONS_KEY][:id]
|
||||
end
|
||||
|
||||
def exists?(env)
|
||||
current_session_id(env).present?
|
||||
end
|
||||
|
||||
def get_session(env, sid)
|
||||
@@ -176,6 +247,30 @@ module ActionController
|
||||
def set_session(env, sid, session_data)
|
||||
raise '#set_session needs to be implemented.'
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
raise '#destroy needs to be implemented.'
|
||||
end
|
||||
|
||||
module SessionUtils
|
||||
private
|
||||
def stale_session_check!
|
||||
yield
|
||||
rescue ArgumentError => argument_error
|
||||
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
|
||||
begin
|
||||
# Note that the regexp does not allow $1 to end with a ':'
|
||||
$1.constantize
|
||||
rescue LoadError, NameError => const_error
|
||||
raise ActionController::SessionRestoreError, "Session contains objects whose class definition isn\\'t available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: \#{const_error.message} [\#{const_error.class}])\n"
|
||||
end
|
||||
retry
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
include SessionUtils
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,6 +36,8 @@ module ActionController
|
||||
#
|
||||
# Note that changing digest or secret invalidates all existing sessions!
|
||||
class CookieStore
|
||||
include AbstractStore::SessionUtils
|
||||
|
||||
# Cookies can typically store 4096 bytes.
|
||||
MAX = 4096
|
||||
SECRET_MIN_LENGTH = 30 # characters
|
||||
@@ -50,7 +52,6 @@ module ActionController
|
||||
|
||||
ENV_SESSION_KEY = "rack.session".freeze
|
||||
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
|
||||
HTTP_SET_COOKIE = "Set-Cookie".freeze
|
||||
|
||||
# Raised when storing more than 4K of session data.
|
||||
class CookieOverflow < StandardError; end
|
||||
@@ -59,12 +60,18 @@ module ActionController
|
||||
# Process legacy CGI options
|
||||
options = options.symbolize_keys
|
||||
if options.has_key?(:session_path)
|
||||
ActiveSupport::Deprecation.warn "Giving :session_path to SessionStore is deprecated, " <<
|
||||
"please use :path instead", caller
|
||||
options[:path] = options.delete(:session_path)
|
||||
end
|
||||
if options.has_key?(:session_key)
|
||||
ActiveSupport::Deprecation.warn "Giving :session_key to SessionStore is deprecated, " <<
|
||||
"please use :key instead", caller
|
||||
options[:key] = options.delete(:session_key)
|
||||
end
|
||||
if options.has_key?(:session_http_only)
|
||||
ActiveSupport::Deprecation.warn "Giving :session_http_only to SessionStore is deprecated, " <<
|
||||
"please use :httponly instead", caller
|
||||
options[:httponly] = options.delete(:session_http_only)
|
||||
end
|
||||
|
||||
@@ -87,73 +94,81 @@ module ActionController
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
|
||||
|
||||
prepare!(env)
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
session_data = env[ENV_SESSION_KEY]
|
||||
options = env[ENV_SESSION_OPTIONS_KEY]
|
||||
request = ActionController::Request.new(env)
|
||||
|
||||
if !(options[:secure] && !request.ssl?) && (!session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after])
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
|
||||
|
||||
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
|
||||
persistent_session_id!(session_data)
|
||||
session_data = marshal(session_data.to_hash)
|
||||
|
||||
raise CookieOverflow if session_data.size > MAX
|
||||
|
||||
cookie = Hash.new
|
||||
cookie[:value] = session_data
|
||||
unless options[:expire_after].nil?
|
||||
cookie[:expires] = Time.now + options[:expire_after]
|
||||
end
|
||||
|
||||
cookie = build_cookie(@key, cookie.merge(options))
|
||||
unless headers[HTTP_SET_COOKIE].blank?
|
||||
headers[HTTP_SET_COOKIE] << "\n#{cookie}"
|
||||
else
|
||||
headers[HTTP_SET_COOKIE] = cookie
|
||||
end
|
||||
Rack::Utils.set_cookie_header!(headers, @key, cookie.merge(options))
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
private
|
||||
# Should be in Rack::Utils soon
|
||||
def build_cookie(key, value)
|
||||
case value
|
||||
when Hash
|
||||
domain = "; domain=" + value[:domain] if value[:domain]
|
||||
path = "; path=" + value[:path] if value[:path]
|
||||
# According to RFC 2109, we need dashes here.
|
||||
# N.B.: cgi.rb uses spaces...
|
||||
expires = "; expires=" + value[:expires].clone.gmtime.
|
||||
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
||||
secure = "; secure" if value[:secure]
|
||||
httponly = "; HttpOnly" if value[:httponly]
|
||||
value = value[:value]
|
||||
end
|
||||
value = [value] unless Array === value
|
||||
cookie = Rack::Utils.escape(key) + "=" +
|
||||
value.map { |v| Rack::Utils.escape(v) }.join("&") +
|
||||
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
||||
|
||||
def prepare!(env)
|
||||
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options)
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
request = Rack::Request.new(env)
|
||||
session_data = request.cookies[@key]
|
||||
data = unmarshal(session_data) || persistent_session_id!({})
|
||||
data = unpacked_cookie_data(env)
|
||||
data = persistent_session_id!(data)
|
||||
[data[:session_id], data]
|
||||
end
|
||||
|
||||
def extract_session_id(env)
|
||||
if data = unpacked_cookie_data(env)
|
||||
persistent_session_id!(data) unless data.empty?
|
||||
data[:session_id]
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def current_session_id(env)
|
||||
env[ENV_SESSION_OPTIONS_KEY][:id]
|
||||
end
|
||||
|
||||
def exists?(env)
|
||||
current_session_id(env).present?
|
||||
end
|
||||
|
||||
def unpacked_cookie_data(env)
|
||||
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
|
||||
stale_session_check! do
|
||||
request = Rack::Request.new(env)
|
||||
session_data = request.cookies[@key]
|
||||
unmarshal(session_data) || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Marshal a session hash into safe cookie data. Include an integrity hash.
|
||||
def marshal(session)
|
||||
@verifier.generate(persistent_session_id!(session))
|
||||
@verifier.generate(session)
|
||||
end
|
||||
|
||||
# Unmarshal cookie data to a hash and verify its integrity.
|
||||
def unmarshal(cookie)
|
||||
persistent_session_id!(@verifier.verify(cookie)) if cookie
|
||||
@verifier.verify(cookie) if cookie
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
nil
|
||||
end
|
||||
@@ -201,6 +216,10 @@ module ActionController
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
# session data is stored on client; nothing to do here
|
||||
end
|
||||
|
||||
def persistent_session_id!(data)
|
||||
(data ||= {}).merge!(inject_persistent_session_id(data))
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
begin
|
||||
require_library_or_gem 'memcache'
|
||||
|
||||
require 'thread'
|
||||
module ActionController
|
||||
module Session
|
||||
class MemCacheStore < AbstractStore
|
||||
@@ -43,6 +43,15 @@ begin
|
||||
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
||||
return false
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
if sid = current_session_id(env)
|
||||
@pool.delete(sid)
|
||||
end
|
||||
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
||||
false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'active_support/core_ext/string/bytesize'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
# Methods for sending arbitrary data and for streaming files to the browser,
|
||||
# instead of rendering.
|
||||
@@ -137,7 +139,7 @@ module ActionController #:nodoc:
|
||||
# instead. See ActionController::Base#render for more information.
|
||||
def send_data(data, options = {}) #:doc:
|
||||
logger.info "Sending data #{options[:filename]}" if logger
|
||||
send_file_headers! options.merge(:length => data.size)
|
||||
send_file_headers! options.merge(:length => data.bytesize)
|
||||
@performed_render = false
|
||||
render :status => options[:status], :text => data
|
||||
end
|
||||
@@ -161,7 +163,7 @@ module ActionController #:nodoc:
|
||||
content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers
|
||||
|
||||
headers.merge!(
|
||||
'Content-Length' => options[:length],
|
||||
'Content-Length' => options[:length].to_s,
|
||||
'Content-Type' => content_type,
|
||||
'Content-Disposition' => disposition,
|
||||
'Content-Transfer-Encoding' => 'binary'
|
||||
|
||||
29
actionpack/lib/action_controller/string_coercion.rb
Normal file
29
actionpack/lib/action_controller/string_coercion.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module ActionController
|
||||
class StringCoercion
|
||||
class UglyBody < ActiveSupport::BasicObject
|
||||
def initialize(body)
|
||||
@body = body
|
||||
end
|
||||
|
||||
def each
|
||||
@body.each do |part|
|
||||
yield part.to_s
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def method_missing(*args, &block)
|
||||
@body.__send__(*args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
[status, headers, UglyBody.new(body)]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -15,12 +15,12 @@
|
||||
show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';"
|
||||
hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"}
|
||||
%>
|
||||
<a href="#" onclick="<%= hide %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
|
||||
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
|
||||
<% end %>
|
||||
|
||||
<% traces.each do |name, trace| %>
|
||||
<div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;">
|
||||
<pre><code><%= trace.join "\n" %></code></pre>
|
||||
<pre><code><%=h trace.join "\n" %></code></pre>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,11 @@ module ActionController
|
||||
class TestCase < ActiveSupport::TestCase
|
||||
include TestProcess
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@controller = nil
|
||||
end
|
||||
|
||||
module Assertions
|
||||
%w(response selector tag dom routing model).each do |kind|
|
||||
include ActionController::Assertions.const_get("#{kind.camelize}Assertions")
|
||||
@@ -195,7 +200,7 @@ module ActionController
|
||||
@controller.send(:initialize_current_url)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local
|
||||
def rescue_action_in_public!
|
||||
@request.remote_addr = '208.77.188.166' # example.com
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require 'rack/session/abstract/id'
|
||||
module ActionController #:nodoc:
|
||||
class TestRequest < Request #:nodoc:
|
||||
attr_accessor :cookies, :session_options
|
||||
@@ -13,6 +14,8 @@ module ActionController #:nodoc:
|
||||
|
||||
@query_parameters = {}
|
||||
@session = TestSession.new
|
||||
default_rack_options = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
|
||||
@session_options ||= {:id => generate_sid(default_rack_options[:sidbits])}.merge(default_rack_options)
|
||||
|
||||
initialize_default_values
|
||||
initialize_containers
|
||||
@@ -88,7 +91,7 @@ module ActionController #:nodoc:
|
||||
@path || super()
|
||||
end
|
||||
|
||||
def assign_parameters(controller_path, action, parameters)
|
||||
def assign_parameters(controller_path, action, parameters = {})
|
||||
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
|
||||
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
|
||||
non_path_parameters = get? ? query_parameters : request_parameters
|
||||
@@ -110,6 +113,7 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def recycle!
|
||||
@env["action_controller.request.request_parameters"] = {}
|
||||
self.query_parameters = {}
|
||||
self.path_parameters = {}
|
||||
@headers, @request_method, @accepts, @content_type = nil, nil, nil, nil
|
||||
@@ -120,6 +124,10 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
private
|
||||
def generate_sid(sidbits)
|
||||
"%0#{sidbits / 4}x" % rand(2**sidbits - 1)
|
||||
end
|
||||
|
||||
def initialize_containers
|
||||
@cookies = {}
|
||||
end
|
||||
@@ -250,7 +258,7 @@ module ActionController #:nodoc:
|
||||
def cookies
|
||||
cookies = {}
|
||||
Array(headers['Set-Cookie']).each do |cookie|
|
||||
key, value = cookie.split(";").first.split("=")
|
||||
key, value = cookie.split(";").first.split("=").map {|val| Rack::Utils.unescape(val)}
|
||||
cookies[key] = value
|
||||
end
|
||||
cookies
|
||||
@@ -442,7 +450,7 @@ module ActionController #:nodoc:
|
||||
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
|
||||
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
|
||||
@request.env['HTTP_ACCEPT'] = [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
|
||||
returning __send__(request_method, action, parameters, session, flash) do
|
||||
__send__(request_method, action, parameters, session, flash).tap do
|
||||
@request.env.delete 'HTTP_X_REQUESTED_WITH'
|
||||
@request.env.delete 'HTTP_ACCEPT'
|
||||
end
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module ActionController
|
||||
module Translation
|
||||
def translate(*args)
|
||||
I18n.translate *args
|
||||
I18n.translate(*args)
|
||||
end
|
||||
alias :t :translate
|
||||
|
||||
def localize(*args)
|
||||
I18n.localize *args
|
||||
I18n.localize(*args)
|
||||
end
|
||||
alias :l :localize
|
||||
end
|
||||
|
||||
@@ -3,14 +3,14 @@ module ActionController
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
attr_accessor :original_path, :content_type
|
||||
alias_method :local_path, :path
|
||||
alias_method :local_path, :path if method_defined?(:path)
|
||||
end
|
||||
end
|
||||
|
||||
def self.extended(object)
|
||||
object.class_eval do
|
||||
attr_accessor :original_path, :content_type
|
||||
alias_method :local_path, :path
|
||||
alias_method :local_path, :path if method_defined?(:path)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'uri'
|
||||
|
||||
module ActionController
|
||||
# In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse
|
||||
# is also possible: an URL can be generated from one of your routing definitions.
|
||||
@@ -92,6 +94,14 @@ module ActionController
|
||||
# end
|
||||
# end
|
||||
module UrlWriter
|
||||
RESERVED_PCHAR = ':@&=+$,;%'
|
||||
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
|
||||
if RUBY_VERSION >= '1.9'
|
||||
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
|
||||
else
|
||||
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
|
||||
end
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
ActionController::Routing::Routes.install_helpers(base)
|
||||
base.mattr_accessor :default_url_options
|
||||
@@ -142,7 +152,7 @@ module ActionController
|
||||
end
|
||||
trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
|
||||
url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
|
||||
anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options[:anchor]
|
||||
anchor = "##{URI.escape(options.delete(:anchor).to_param.to_s, UNSAFE_PCHAR)}" if options[:anchor]
|
||||
generated = Routing::Routes.generate(options, {})
|
||||
url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
|
||||
url << anchor if anchor
|
||||
@@ -159,6 +169,9 @@ module ActionController
|
||||
end
|
||||
|
||||
def rewrite(options = {})
|
||||
if options.include?(:overwrite_params)
|
||||
ActiveSupport::Deprecation.warn 'The :overwrite_params option is deprecated. Specify all the necessary parameters instead', caller
|
||||
end
|
||||
rewrite_url(options)
|
||||
end
|
||||
|
||||
@@ -184,7 +197,7 @@ module ActionController
|
||||
path = rewrite_path(options)
|
||||
rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
|
||||
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
|
||||
rewritten_url << "##{options[:anchor]}" if options[:anchor]
|
||||
rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor]
|
||||
|
||||
rewritten_url
|
||||
end
|
||||
@@ -194,7 +207,7 @@ module ActionController
|
||||
options = options.symbolize_keys
|
||||
options.update(options[:params].symbolize_keys) if options[:params]
|
||||
|
||||
if (overwrite = options.delete(:overwrite_params))
|
||||
if overwrite = options.delete(:overwrite_params)
|
||||
options.update(@parameters.symbolize_keys)
|
||||
options.update(overwrite.symbolize_keys)
|
||||
end
|
||||
|
||||
@@ -162,7 +162,7 @@ module HTML #:nodoc:
|
||||
end
|
||||
|
||||
closing = ( scanner.scan(/\//) ? :close : nil )
|
||||
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
|
||||
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
|
||||
name.downcase!
|
||||
|
||||
unless closing
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Copyright (C) 2007, 2008, 2009 Christian Neukirchen <purl.org/net/chneukirchen>
|
||||
#
|
||||
# Rack is freely distributable under the terms of an MIT-style license.
|
||||
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
$:.unshift(File.expand_path(File.dirname(__FILE__)))
|
||||
|
||||
|
||||
# The Rack main module, serving as a namespace for all core Rack
|
||||
# modules and classes.
|
||||
#
|
||||
# All modules meant for use in your application are <tt>autoload</tt>ed here,
|
||||
# so it should be enough just to <tt>require rack.rb</tt> in your code.
|
||||
|
||||
module Rack
|
||||
# The Rack protocol version number implemented.
|
||||
VERSION = [0,1]
|
||||
|
||||
# Return the Rack protocol version as a dotted string.
|
||||
def self.version
|
||||
VERSION.join(".")
|
||||
end
|
||||
|
||||
# Return the Rack release as a dotted string.
|
||||
def self.release
|
||||
"1.0 bundled"
|
||||
end
|
||||
|
||||
autoload :Builder, "rack/builder"
|
||||
autoload :Cascade, "rack/cascade"
|
||||
autoload :Chunked, "rack/chunked"
|
||||
autoload :CommonLogger, "rack/commonlogger"
|
||||
autoload :ConditionalGet, "rack/conditionalget"
|
||||
autoload :ContentLength, "rack/content_length"
|
||||
autoload :ContentType, "rack/content_type"
|
||||
autoload :File, "rack/file"
|
||||
autoload :Deflater, "rack/deflater"
|
||||
autoload :Directory, "rack/directory"
|
||||
autoload :ForwardRequest, "rack/recursive"
|
||||
autoload :Handler, "rack/handler"
|
||||
autoload :Head, "rack/head"
|
||||
autoload :Lint, "rack/lint"
|
||||
autoload :Lock, "rack/lock"
|
||||
autoload :MethodOverride, "rack/methodoverride"
|
||||
autoload :Mime, "rack/mime"
|
||||
autoload :Recursive, "rack/recursive"
|
||||
autoload :Reloader, "rack/reloader"
|
||||
autoload :ShowExceptions, "rack/showexceptions"
|
||||
autoload :ShowStatus, "rack/showstatus"
|
||||
autoload :Static, "rack/static"
|
||||
autoload :URLMap, "rack/urlmap"
|
||||
autoload :Utils, "rack/utils"
|
||||
|
||||
autoload :MockRequest, "rack/mock"
|
||||
autoload :MockResponse, "rack/mock"
|
||||
|
||||
autoload :Request, "rack/request"
|
||||
autoload :Response, "rack/response"
|
||||
|
||||
module Auth
|
||||
autoload :Basic, "rack/auth/basic"
|
||||
autoload :AbstractRequest, "rack/auth/abstract/request"
|
||||
autoload :AbstractHandler, "rack/auth/abstract/handler"
|
||||
autoload :OpenID, "rack/auth/openid"
|
||||
module Digest
|
||||
autoload :MD5, "rack/auth/digest/md5"
|
||||
autoload :Nonce, "rack/auth/digest/nonce"
|
||||
autoload :Params, "rack/auth/digest/params"
|
||||
autoload :Request, "rack/auth/digest/request"
|
||||
end
|
||||
end
|
||||
|
||||
module Session
|
||||
autoload :Cookie, "rack/session/cookie"
|
||||
autoload :Pool, "rack/session/pool"
|
||||
autoload :Memcache, "rack/session/memcache"
|
||||
end
|
||||
|
||||
# *Adapters* connect Rack with third party web frameworks.
|
||||
#
|
||||
# Rack includes an adapter for Camping, see README for other
|
||||
# frameworks supporting Rack in their code bases.
|
||||
#
|
||||
# Refer to the submodules for framework-specific calling details.
|
||||
|
||||
module Adapter
|
||||
autoload :Camping, "rack/adapter/camping"
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
module Rack
|
||||
module Adapter
|
||||
class Camping
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env["PATH_INFO"] ||= ""
|
||||
env["SCRIPT_NAME"] ||= ""
|
||||
controller = @app.run(env['rack.input'], env)
|
||||
h = controller.headers
|
||||
h.each_pair do |k,v|
|
||||
if v.kind_of? URI
|
||||
h[k] = v.to_s
|
||||
end
|
||||
end
|
||||
[controller.status, controller.headers, [controller.body.to_s]]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module Rack
|
||||
module Auth
|
||||
# Rack::Auth::AbstractHandler implements common authentication functionality.
|
||||
#
|
||||
# +realm+ should be set for all handlers.
|
||||
|
||||
class AbstractHandler
|
||||
|
||||
attr_accessor :realm
|
||||
|
||||
def initialize(app, realm=nil, &authenticator)
|
||||
@app, @realm, @authenticator = app, realm, authenticator
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def unauthorized(www_authenticate = challenge)
|
||||
return [ 401,
|
||||
{ 'Content-Type' => 'text/plain',
|
||||
'Content-Length' => '0',
|
||||
'WWW-Authenticate' => www_authenticate.to_s },
|
||||
[]
|
||||
]
|
||||
end
|
||||
|
||||
def bad_request
|
||||
return [ 400,
|
||||
{ 'Content-Type' => 'text/plain',
|
||||
'Content-Length' => '0' },
|
||||
[]
|
||||
]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module Rack
|
||||
module Auth
|
||||
class AbstractRequest
|
||||
|
||||
def initialize(env)
|
||||
@env = env
|
||||
end
|
||||
|
||||
def provided?
|
||||
!authorization_key.nil?
|
||||
end
|
||||
|
||||
def parts
|
||||
@parts ||= @env[authorization_key].split(' ', 2)
|
||||
end
|
||||
|
||||
def scheme
|
||||
@scheme ||= parts.first.downcase.to_sym
|
||||
end
|
||||
|
||||
def params
|
||||
@params ||= parts.last
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
|
||||
|
||||
def authorization_key
|
||||
@authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,58 +0,0 @@
|
||||
require 'rack/auth/abstract/handler'
|
||||
require 'rack/auth/abstract/request'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
# Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617.
|
||||
#
|
||||
# Initialize with the Rack application that you want protecting,
|
||||
# and a block that checks if a username and password pair are valid.
|
||||
#
|
||||
# See also: <tt>example/protectedlobster.rb</tt>
|
||||
|
||||
class Basic < AbstractHandler
|
||||
|
||||
def call(env)
|
||||
auth = Basic::Request.new(env)
|
||||
|
||||
return unauthorized unless auth.provided?
|
||||
|
||||
return bad_request unless auth.basic?
|
||||
|
||||
if valid?(auth)
|
||||
env['REMOTE_USER'] = auth.username
|
||||
|
||||
return @app.call(env)
|
||||
end
|
||||
|
||||
unauthorized
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def challenge
|
||||
'Basic realm="%s"' % realm
|
||||
end
|
||||
|
||||
def valid?(auth)
|
||||
@authenticator.call(*auth.credentials)
|
||||
end
|
||||
|
||||
class Request < Auth::AbstractRequest
|
||||
def basic?
|
||||
:basic == scheme
|
||||
end
|
||||
|
||||
def credentials
|
||||
@credentials ||= params.unpack("m*").first.split(/:/, 2)
|
||||
end
|
||||
|
||||
def username
|
||||
credentials.first
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,124 +0,0 @@
|
||||
require 'rack/auth/abstract/handler'
|
||||
require 'rack/auth/digest/request'
|
||||
require 'rack/auth/digest/params'
|
||||
require 'rack/auth/digest/nonce'
|
||||
require 'digest/md5'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
# Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
|
||||
# HTTP Digest Authentication, as per RFC 2617.
|
||||
#
|
||||
# Initialize with the [Rack] application that you want protecting,
|
||||
# and a block that looks up a plaintext password for a given username.
|
||||
#
|
||||
# +opaque+ needs to be set to a constant base64/hexadecimal string.
|
||||
#
|
||||
class MD5 < AbstractHandler
|
||||
|
||||
attr_accessor :opaque
|
||||
|
||||
attr_writer :passwords_hashed
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@passwords_hashed = nil
|
||||
end
|
||||
|
||||
def passwords_hashed?
|
||||
!!@passwords_hashed
|
||||
end
|
||||
|
||||
def call(env)
|
||||
auth = Request.new(env)
|
||||
|
||||
unless auth.provided?
|
||||
return unauthorized
|
||||
end
|
||||
|
||||
if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
|
||||
return bad_request
|
||||
end
|
||||
|
||||
if valid?(auth)
|
||||
if auth.nonce.stale?
|
||||
return unauthorized(challenge(:stale => true))
|
||||
else
|
||||
env['REMOTE_USER'] = auth.username
|
||||
|
||||
return @app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
unauthorized
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
QOP = 'auth'.freeze
|
||||
|
||||
def params(hash = {})
|
||||
Params.new do |params|
|
||||
params['realm'] = realm
|
||||
params['nonce'] = Nonce.new.to_s
|
||||
params['opaque'] = H(opaque)
|
||||
params['qop'] = QOP
|
||||
|
||||
hash.each { |k, v| params[k] = v }
|
||||
end
|
||||
end
|
||||
|
||||
def challenge(hash = {})
|
||||
"Digest #{params(hash)}"
|
||||
end
|
||||
|
||||
def valid?(auth)
|
||||
valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
|
||||
end
|
||||
|
||||
def valid_qop?(auth)
|
||||
QOP == auth.qop
|
||||
end
|
||||
|
||||
def valid_opaque?(auth)
|
||||
H(opaque) == auth.opaque
|
||||
end
|
||||
|
||||
def valid_nonce?(auth)
|
||||
auth.nonce.valid?
|
||||
end
|
||||
|
||||
def valid_digest?(auth)
|
||||
digest(auth, @authenticator.call(auth.username)) == auth.response
|
||||
end
|
||||
|
||||
def md5(data)
|
||||
::Digest::MD5.hexdigest(data)
|
||||
end
|
||||
|
||||
alias :H :md5
|
||||
|
||||
def KD(secret, data)
|
||||
H([secret, data] * ':')
|
||||
end
|
||||
|
||||
def A1(auth, password)
|
||||
[ auth.username, auth.realm, password ] * ':'
|
||||
end
|
||||
|
||||
def A2(auth)
|
||||
[ auth.method, auth.uri ] * ':'
|
||||
end
|
||||
|
||||
def digest(auth, password)
|
||||
password_hash = passwords_hashed? ? password : H(A1(auth, password))
|
||||
|
||||
KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':')
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
require 'digest/md5'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
# Rack::Auth::Digest::Nonce is the default nonce generator for the
|
||||
# Rack::Auth::Digest::MD5 authentication handler.
|
||||
#
|
||||
# +private_key+ needs to set to a constant string.
|
||||
#
|
||||
# +time_limit+ can be optionally set to an integer (number of seconds),
|
||||
# to limit the validity of the generated nonces.
|
||||
|
||||
class Nonce
|
||||
|
||||
class << self
|
||||
attr_accessor :private_key, :time_limit
|
||||
end
|
||||
|
||||
def self.parse(string)
|
||||
new(*string.unpack("m*").first.split(' ', 2))
|
||||
end
|
||||
|
||||
def initialize(timestamp = Time.now, given_digest = nil)
|
||||
@timestamp, @given_digest = timestamp.to_i, given_digest
|
||||
end
|
||||
|
||||
def to_s
|
||||
[([ @timestamp, digest ] * ' ')].pack("m*").strip
|
||||
end
|
||||
|
||||
def digest
|
||||
::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':')
|
||||
end
|
||||
|
||||
def valid?
|
||||
digest == @given_digest
|
||||
end
|
||||
|
||||
def stale?
|
||||
!self.class.time_limit.nil? && (@timestamp - Time.now.to_i) < self.class.time_limit
|
||||
end
|
||||
|
||||
def fresh?
|
||||
!stale?
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
class Params < Hash
|
||||
|
||||
def self.parse(str)
|
||||
split_header_value(str).inject(new) do |header, param|
|
||||
k, v = param.split('=', 2)
|
||||
header[k] = dequote(v)
|
||||
header
|
||||
end
|
||||
end
|
||||
|
||||
def self.dequote(str) # From WEBrick::HTTPUtils
|
||||
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
||||
ret.gsub!(/\\(.)/, "\\1")
|
||||
ret
|
||||
end
|
||||
|
||||
def self.split_header_value(str)
|
||||
str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] }
|
||||
end
|
||||
|
||||
def initialize
|
||||
super
|
||||
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
def [](k)
|
||||
super k.to_s
|
||||
end
|
||||
|
||||
def []=(k, v)
|
||||
super k.to_s, v.to_s
|
||||
end
|
||||
|
||||
UNQUOTED = ['qop', 'nc', 'stale']
|
||||
|
||||
def to_s
|
||||
inject([]) do |parts, (k, v)|
|
||||
parts << "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v))
|
||||
parts
|
||||
end.join(', ')
|
||||
end
|
||||
|
||||
def quote(str) # From WEBrick::HTTPUtils
|
||||
'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
require 'rack/auth/abstract/request'
|
||||
require 'rack/auth/digest/params'
|
||||
require 'rack/auth/digest/nonce'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
class Request < Auth::AbstractRequest
|
||||
|
||||
def method
|
||||
@env['rack.methodoverride.original_method'] || @env['REQUEST_METHOD']
|
||||
end
|
||||
|
||||
def digest?
|
||||
:digest == scheme
|
||||
end
|
||||
|
||||
def correct_uri?
|
||||
(@env['SCRIPT_NAME'].to_s + @env['PATH_INFO'].to_s) == uri
|
||||
end
|
||||
|
||||
def nonce
|
||||
@nonce ||= Nonce.parse(params['nonce'])
|
||||
end
|
||||
|
||||
def params
|
||||
@params ||= Params.parse(parts.last)
|
||||
end
|
||||
|
||||
def method_missing(sym)
|
||||
if params.has_key? key = sym.to_s
|
||||
return params[key]
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,480 +0,0 @@
|
||||
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
||||
|
||||
gem 'ruby-openid', '~> 2' if defined? Gem
|
||||
require 'rack/request'
|
||||
require 'rack/utils'
|
||||
require 'rack/auth/abstract/handler'
|
||||
require 'uri'
|
||||
require 'openid' #gem
|
||||
require 'openid/extension' #gem
|
||||
require 'openid/store/memory' #gem
|
||||
|
||||
module Rack
|
||||
class Request
|
||||
def openid_request
|
||||
@env['rack.auth.openid.request']
|
||||
end
|
||||
|
||||
def openid_response
|
||||
@env['rack.auth.openid.response']
|
||||
end
|
||||
end
|
||||
|
||||
module Auth
|
||||
|
||||
# Rack::Auth::OpenID provides a simple method for setting up an OpenID
|
||||
# Consumer. It requires the ruby-openid library from janrain to operate,
|
||||
# as well as a rack method of session management.
|
||||
#
|
||||
# The ruby-openid home page is at http://openidenabled.com/ruby-openid/.
|
||||
#
|
||||
# The OpenID specifications can be found at
|
||||
# http://openid.net/specs/openid-authentication-1_1.html
|
||||
# and
|
||||
# http://openid.net/specs/openid-authentication-2_0.html. Documentation
|
||||
# for published OpenID extensions and related topics can be found at
|
||||
# http://openid.net/developers/specs/.
|
||||
#
|
||||
# It is recommended to read through the OpenID spec, as well as
|
||||
# ruby-openid's documentation, to understand what exactly goes on. However
|
||||
# a setup as simple as the presented examples is enough to provide
|
||||
# Consumer functionality.
|
||||
#
|
||||
# This library strongly intends to utilize the OpenID 2.0 features of the
|
||||
# ruby-openid library, which provides OpenID 1.0 compatiblity.
|
||||
#
|
||||
# NOTE: Due to the amount of data that this library stores in the
|
||||
# session, Rack::Session::Cookie may fault.
|
||||
|
||||
class OpenID
|
||||
|
||||
class NoSession < RuntimeError; end
|
||||
class BadExtension < RuntimeError; end
|
||||
# Required for ruby-openid
|
||||
ValidStatus = [:success, :setup_needed, :cancel, :failure]
|
||||
|
||||
# = Arguments
|
||||
#
|
||||
# The first argument is the realm, identifying the site they are trusting
|
||||
# with their identity. This is required, also treated as the trust_root
|
||||
# in OpenID 1.x exchanges.
|
||||
#
|
||||
# The optional second argument is a hash of options.
|
||||
#
|
||||
# == Options
|
||||
#
|
||||
# <tt>:return_to</tt> defines the url to return to after the client
|
||||
# authenticates with the openid service provider. This url should point
|
||||
# to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not
|
||||
# provided, return_to will be the current url which allows flexibility
|
||||
# with caveats.
|
||||
#
|
||||
# <tt>:session_key</tt> defines the key to the session hash in the env.
|
||||
# It defaults to 'rack.session'.
|
||||
#
|
||||
# <tt>:openid_param</tt> defines at what key in the request parameters to
|
||||
# find the identifier to resolve. As per the 2.0 spec, the default is
|
||||
# 'openid_identifier'.
|
||||
#
|
||||
# <tt>:store</tt> defined what OpenID Store to use for persistant
|
||||
# information. By default a Store::Memory will be used.
|
||||
#
|
||||
# <tt>:immediate</tt> as true will make initial requests to be of an
|
||||
# immediate type. This is false by default. See OpenID specification
|
||||
# documentation.
|
||||
#
|
||||
# <tt>:extensions</tt> should be a hash of openid extension
|
||||
# implementations. The key should be the extension main module, the value
|
||||
# should be an array of arguments for extension::Request.new.
|
||||
# The hash is iterated over and passed to #add_extension for processing.
|
||||
# Please see #add_extension for further documentation.
|
||||
#
|
||||
# == Examples
|
||||
#
|
||||
# simple_oid = OpenID.new('http://mysite.com/')
|
||||
#
|
||||
# return_oid = OpenID.new('http://mysite.com/', {
|
||||
# :return_to => 'http://mysite.com/openid'
|
||||
# })
|
||||
#
|
||||
# complex_oid = OpenID.new('http://mysite.com/',
|
||||
# :immediate => true,
|
||||
# :extensions => {
|
||||
# ::OpenID::SReg => [['email'],['nickname']]
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# = Advanced
|
||||
#
|
||||
# Most of the functionality of this library is encapsulated such that
|
||||
# expansion and overriding functions isn't difficult nor tricky.
|
||||
# Alternately, to avoid opening up singleton objects or subclassing, a
|
||||
# wrapper rack middleware can be composed to act upon Auth::OpenID's
|
||||
# responses. See #check and #finish for locations of pertinent data.
|
||||
#
|
||||
# == Responses
|
||||
#
|
||||
# To change the responses that Auth::OpenID returns, override the methods
|
||||
# #redirect, #bad_request, #unauthorized, #access_denied, and
|
||||
# #foreign_server_failure.
|
||||
#
|
||||
# Additionally #confirm_post_params is used when the URI would exceed
|
||||
# length limits on a GET request when doing the initial verification
|
||||
# request.
|
||||
#
|
||||
# == Processing
|
||||
#
|
||||
# To change methods of processing completed transactions, override the
|
||||
# methods #success, #setup_needed, #cancel, and #failure. Please ensure
|
||||
# the returned object is a rack compatible response.
|
||||
#
|
||||
# The first argument is an OpenID::Response, the second is a
|
||||
# Rack::Request of the current request, the last is the hash used in
|
||||
# ruby-openid handling, which can be found manually at
|
||||
# env['rack.session'][:openid].
|
||||
#
|
||||
# This is useful if you wanted to expand the processing done, such as
|
||||
# setting up user accounts.
|
||||
#
|
||||
# oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
|
||||
# def oid_app.success oid, request, session
|
||||
# user = Models::User[oid.identity_url]
|
||||
# user ||= Models::User.create_from_openid oid
|
||||
# request['rack.session'][:user] = user.id
|
||||
# redirect MyApp.site_home
|
||||
# end
|
||||
#
|
||||
# site_map['/openid'] = oid_app
|
||||
# map = Rack::URLMap.new site_map
|
||||
# ...
|
||||
|
||||
def initialize(realm, options={})
|
||||
realm = URI(realm)
|
||||
raise ArgumentError, "Invalid realm: #{realm}" \
|
||||
unless realm.absolute? \
|
||||
and realm.fragment.nil? \
|
||||
and realm.scheme =~ /^https?$/ \
|
||||
and realm.host =~ /^(\*\.)?#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+/
|
||||
realm.path = '/' if realm.path.empty?
|
||||
@realm = realm.to_s
|
||||
|
||||
if ruri = options[:return_to]
|
||||
ruri = URI(ruri)
|
||||
raise ArgumentError, "Invalid return_to: #{ruri}" \
|
||||
unless ruri.absolute? \
|
||||
and ruri.scheme =~ /^https?$/ \
|
||||
and ruri.fragment.nil?
|
||||
raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
|
||||
unless self.within_realm?(ruri)
|
||||
@return_to = ruri.to_s
|
||||
end
|
||||
|
||||
@session_key = options[:session_key] || 'rack.session'
|
||||
@openid_param = options[:openid_param] || 'openid_identifier'
|
||||
@store = options[:store] || ::OpenID::Store::Memory.new
|
||||
@immediate = !!options[:immediate]
|
||||
|
||||
@extensions = {}
|
||||
if extensions = options.delete(:extensions)
|
||||
extensions.each do |ext, args|
|
||||
add_extension ext, *args
|
||||
end
|
||||
end
|
||||
|
||||
# Undocumented, semi-experimental
|
||||
@anonymous = !!options[:anonymous]
|
||||
end
|
||||
|
||||
attr_reader :realm, :return_to, :session_key, :openid_param, :store,
|
||||
:immediate, :extensions
|
||||
|
||||
# Sets up and uses session data at <tt>:openid</tt> within the session.
|
||||
# Errors in this setup will raise a NoSession exception.
|
||||
#
|
||||
# If the parameter 'openid.mode' is set, which implies a followup from
|
||||
# the openid server, processing is passed to #finish and the result is
|
||||
# returned. However, if there is no appropriate openid information in the
|
||||
# session, a 400 error is returned.
|
||||
#
|
||||
# If the parameter specified by <tt>options[:openid_param]</tt> is
|
||||
# present, processing is passed to #check and the result is returned.
|
||||
#
|
||||
# If neither of these conditions are met, #unauthorized is called.
|
||||
|
||||
def call(env)
|
||||
env['rack.auth.openid'] = self
|
||||
env_session = env[@session_key]
|
||||
unless env_session and env_session.is_a?(Hash)
|
||||
raise NoSession, 'No compatible session'
|
||||
end
|
||||
# let us work in our own namespace...
|
||||
session = (env_session[:openid] ||= {})
|
||||
unless session and session.is_a?(Hash)
|
||||
raise NoSession, 'Incompatible openid session'
|
||||
end
|
||||
|
||||
request = Rack::Request.new(env)
|
||||
consumer = ::OpenID::Consumer.new(session, @store)
|
||||
|
||||
if mode = request.GET['openid.mode']
|
||||
if session.key?(:openid_param)
|
||||
finish(consumer, session, request)
|
||||
else
|
||||
bad_request
|
||||
end
|
||||
elsif request.GET[@openid_param]
|
||||
check(consumer, session, request)
|
||||
else
|
||||
unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
# As the first part of OpenID consumer action, #check retrieves the data
|
||||
# required for completion.
|
||||
#
|
||||
# If all parameters fit within the max length of a URI, a 303 redirect
|
||||
# will be returned. Otherwise #confirm_post_params will be called.
|
||||
#
|
||||
# Any messages from OpenID's request are logged to env['rack.errors']
|
||||
#
|
||||
# <tt>env['rack.auth.openid.request']</tt> is the openid checkid request
|
||||
# instance.
|
||||
#
|
||||
# <tt>session[:openid_param]</tt> is set to the openid identifier
|
||||
# provided by the user.
|
||||
#
|
||||
# <tt>session[:return_to]</tt> is set to the return_to uri given to the
|
||||
# identity provider.
|
||||
|
||||
def check(consumer, session, req)
|
||||
oid = consumer.begin(req.GET[@openid_param], @anonymous)
|
||||
req.env['rack.auth.openid.request'] = oid
|
||||
req.env['rack.errors'].puts(oid.message)
|
||||
p oid if $DEBUG
|
||||
|
||||
## Extension support
|
||||
extensions.each do |ext,args|
|
||||
oid.add_extension(ext::Request.new(*args))
|
||||
end
|
||||
|
||||
session[:openid_param] = req.GET[openid_param]
|
||||
return_to_uri = return_to ? return_to : req.url
|
||||
session[:return_to] = return_to_uri
|
||||
immediate = session.key?(:setup_needed) ? false : immediate
|
||||
|
||||
if oid.send_redirect?(realm, return_to_uri, immediate)
|
||||
uri = oid.redirect_url(realm, return_to_uri, immediate)
|
||||
redirect(uri)
|
||||
else
|
||||
confirm_post_params(oid, realm, return_to_uri, immediate)
|
||||
end
|
||||
rescue ::OpenID::DiscoveryFailure => e
|
||||
# thrown from inside OpenID::Consumer#begin by yadis stuff
|
||||
req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n")
|
||||
return foreign_server_failure
|
||||
end
|
||||
|
||||
# This is the final portion of authentication.
|
||||
# If successful, a redirect to the realm is be returned.
|
||||
# Data gathered from extensions are stored in session[:openid] with the
|
||||
# extension's namespace uri as the key.
|
||||
#
|
||||
# Any messages from OpenID's response are logged to env['rack.errors']
|
||||
#
|
||||
# <tt>env['rack.auth.openid.response']</tt> will contain the openid
|
||||
# response.
|
||||
|
||||
def finish(consumer, session, req)
|
||||
oid = consumer.complete(req.GET, req.url)
|
||||
req.env['rack.auth.openid.response'] = oid
|
||||
req.env['rack.errors'].puts(oid.message)
|
||||
p oid if $DEBUG
|
||||
|
||||
raise unless ValidStatus.include?(oid.status)
|
||||
__send__(oid.status, oid, req, session)
|
||||
end
|
||||
|
||||
# The first argument should be the main extension module.
|
||||
# The extension module should contain the constants:
|
||||
# * class Request, should have OpenID::Extension as an ancestor
|
||||
# * class Response, should have OpenID::Extension as an ancestor
|
||||
# * string NS_URI, which defining the namespace of the extension
|
||||
#
|
||||
# All trailing arguments will be passed to extension::Request.new in
|
||||
# #check.
|
||||
# The openid response will be passed to
|
||||
# extension::Response#from_success_response, #get_extension_args will be
|
||||
# called on the result to attain the gathered data.
|
||||
#
|
||||
# This method returns the key at which the response data will be found in
|
||||
# the session, which is the namespace uri by default.
|
||||
|
||||
def add_extension(ext, *args)
|
||||
raise BadExtension unless valid_extension?(ext)
|
||||
extensions[ext] = args
|
||||
return ext::NS_URI
|
||||
end
|
||||
|
||||
# Checks the validitity, in the context of usage, of a submitted
|
||||
# extension.
|
||||
|
||||
def valid_extension?(ext)
|
||||
if not %w[NS_URI Request Response].all?{|c| ext.const_defined?(c) }
|
||||
raise ArgumentError, 'Extension is missing constants.'
|
||||
elsif not ext::Response.respond_to?(:from_success_response)
|
||||
raise ArgumentError, 'Response is missing required method.'
|
||||
end
|
||||
return true
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
|
||||
# Checks the provided uri to ensure it'd be considered within the realm.
|
||||
# is currently not compatible with wildcard realms.
|
||||
|
||||
def within_realm? uri
|
||||
uri = URI.parse(uri.to_s)
|
||||
realm = URI.parse(self.realm)
|
||||
return false unless uri.absolute?
|
||||
return false unless uri.path[0, realm.path.size] == realm.path
|
||||
return false unless uri.host == realm.host or realm.host[/^\*\./]
|
||||
# for wildcard support, is awkward with URI limitations
|
||||
realm_match = Regexp.escape(realm.host).
|
||||
sub(/^\*\./,"^#{URI::REGEXP::PATTERN::URIC_NO_SLASH}+.")+'$'
|
||||
return false unless uri.host.match(realm_match)
|
||||
return true
|
||||
end
|
||||
alias_method :include?, :within_realm?
|
||||
|
||||
protected
|
||||
|
||||
### These methods define some of the boilerplate responses.
|
||||
|
||||
# Returns an html form page for posting to an Identity Provider if the
|
||||
# GET request would exceed the upper URI length limit.
|
||||
|
||||
def confirm_post_params(oid, realm, return_to, immediate)
|
||||
Rack::Response.new.finish do |r|
|
||||
r.write '<html><head><title>Confirm...</title></head><body>'
|
||||
r.write oid.form_markup(realm, return_to, immediate)
|
||||
r.write '</body></html>'
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a 303 redirect with the destination of that provided by the
|
||||
# argument.
|
||||
|
||||
def redirect(uri)
|
||||
[ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain',
|
||||
'Location' => uri},
|
||||
[] ]
|
||||
end
|
||||
|
||||
# Returns an empty 400 response.
|
||||
|
||||
def bad_request
|
||||
[ 400, {'Content-Type'=>'text/plain', 'Content-Length'=>'0'},
|
||||
[''] ]
|
||||
end
|
||||
|
||||
# Returns a basic unauthorized 401 response.
|
||||
|
||||
def unauthorized
|
||||
[ 401, {'Content-Type' => 'text/plain', 'Content-Length' => '13'},
|
||||
['Unauthorized.'] ]
|
||||
end
|
||||
|
||||
# Returns a basic access denied 403 response.
|
||||
|
||||
def access_denied
|
||||
[ 403, {'Content-Type' => 'text/plain', 'Content-Length' => '14'},
|
||||
['Access denied.'] ]
|
||||
end
|
||||
|
||||
# Returns a 503 response to be used if communication with the remote
|
||||
# OpenID server fails.
|
||||
|
||||
def foreign_server_failure
|
||||
[ 503, {'Content-Type'=>'text/plain', 'Content-Length' => '23'},
|
||||
['Foreign server failure.'] ]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
### These methods are called after a transaction is completed, depending
|
||||
# on its outcome. These should all return a rack compatible response.
|
||||
# You'd want to override these to provide additional functionality.
|
||||
|
||||
# Called to complete processing on a successful transaction.
|
||||
# Within the openid session, :openid_identity and :openid_identifier are
|
||||
# set to the user friendly and the standard representation of the
|
||||
# validated identity. All other data in the openid session is cleared.
|
||||
|
||||
def success(oid, request, session)
|
||||
session.clear
|
||||
session[:openid_identity] = oid.display_identifier
|
||||
session[:openid_identifier] = oid.identity_url
|
||||
extensions.keys.each do |ext|
|
||||
label = ext.name[/[^:]+$/].downcase
|
||||
response = ext::Response.from_success_response(oid)
|
||||
session[label] = response.data
|
||||
end
|
||||
redirect(realm)
|
||||
end
|
||||
|
||||
# Called if the Identity Provider indicates further setup by the user is
|
||||
# required.
|
||||
# The identifier is retrived from the openid session at :openid_param.
|
||||
# And :setup_needed is set to true to prevent looping.
|
||||
|
||||
def setup_needed(oid, request, session)
|
||||
identifier = session[:openid_param]
|
||||
session[:setup_needed] = true
|
||||
redirect req.script_name + '?' + openid_param + '=' + identifier
|
||||
end
|
||||
|
||||
# Called if the user indicates they wish to cancel identification.
|
||||
# Data within openid session is cleared.
|
||||
|
||||
def cancel(oid, request, session)
|
||||
session.clear
|
||||
access_denied
|
||||
end
|
||||
|
||||
# Called if the Identity Provider indicates the user is unable to confirm
|
||||
# their identity. Data within the openid session is left alone, in case
|
||||
# of swarm auth attacks.
|
||||
|
||||
def failure(oid, request, session)
|
||||
unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
# A class developed out of the request to use OpenID as an authentication
|
||||
# middleware. The request will be sent to the OpenID instance unless the
|
||||
# block evaluates to true. For example in rackup, you can use it as such:
|
||||
#
|
||||
# use Rack::Session::Pool
|
||||
# use Rack::Auth::OpenIDAuth, realm, openid_options do |env|
|
||||
# env['rack.session'][:authkey] == a_string
|
||||
# end
|
||||
# run RackApp
|
||||
#
|
||||
# Or simply:
|
||||
#
|
||||
# app = Rack::Auth::OpenIDAuth.new app, realm, openid_options, &auth
|
||||
|
||||
class OpenIDAuth < Rack::Auth::AbstractHandler
|
||||
attr_reader :oid
|
||||
def initialize(app, realm, options={}, &auth)
|
||||
@oid = OpenID.new(realm, options)
|
||||
super(app, &auth)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
to = auth.call(env) ? @app : @oid
|
||||
to.call env
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user