1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//! `rust-gravatar` is a small Rust library that generates Gravatar image URLs based on the
//! [official Gravatar specification](https://en.gravatar.com/site/implement/images/).
//!
//! Example
//! --------
//! ```
//! extern crate gravatar;
//! use gravatar::{Gravatar, Rating};
//!
//! let url = Gravatar::new("email@example.com")
//!     .set_size(Some(150))
//!     .set_rating(Some(Rating::Pg))
//!     .image_url();
//! assert_eq!(
//!     url.as_str(),
//!     "https://secure.gravatar.com/avatar/5658ffccee7f0ebfda2b226238b1eb6e?s=150&r=pg"
//! );
//! ```

extern crate md5;
extern crate url;

use md5::{Digest, Md5};
use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use url::Url;

/// The default image to display if the user's email does not have a Gravatar.
///
/// See <https://en.gravatar.com/site/implement/images/#default-image>.
#[derive(Clone, Debug)]
pub enum Default {
    /// The URL of an image file to display as the default.
    ImageUrl(Url),

    /// Instead of loading an image, the Gravatar URL will return an HTTP 404 (File Not Found)
    /// response if the email is not found.
    Http404,

    /// A transparent PNG image.
    Blank,

    /// A simple, cartoon-style silhouetted outline of a person that does not vary by email hash.
    MysteryMan,

    /// A geometric pattern based on the email hash.
    Identicon,

    /// A "monster" with different colors, faces, etc. that are generated by the email hash.
    MonsterId,

    /// A face with different features and backgrounds, generated by the email hash.
    Wavatar,

    /// An 8-bit arcade-style pixelated face that is generated by the email hash.
    Retro,
}

/// The maximum rating level for which Gravatar will show the user's image instead of the specified
/// default.
///
/// See <https://en.gravatar.com/site/implement/images/#rating>.
#[derive(Clone, Debug)]
pub enum Rating {
    /// Show "G"-rated images only.
    G,

    /// Show "PG"-rated images or lower only.
    Pg,

    /// Show "R"-rated images or lower only.
    R,

    /// Show all images, up to and including "X"-rated ones.
    X,
}

/// Representation of a single Gravatar image URL.
#[derive(Clone, Debug)]
pub struct Gravatar {
    email: String,
    size: Option<u16>,
    default: Option<Default>,
    force_default: bool,
    rating: Option<Rating>,
    ssl: bool,
}

impl Gravatar {
    /// Creates a new Gravatar with the given email and default values for the other parameters.
    pub fn new(email: &str) -> Gravatar {
        Gravatar {
            email: email.to_string(),
            size: None,
            default: None,
            force_default: false,
            rating: None,
            ssl: true,
        }
    }

    /// Sets the desired image size. If `None` is provided, then no size is passed to Gravatar,
    /// which will then use a default of 80px by 80px. Gravatar will only provide images between
    /// 1px and 2048px by size, so this function will use 1px if the desired size is less than that
    /// and 2048px if the desired size is greater than that.
    ///
    /// For more information, see <https://en.gravatar.com/site/implement/images/#size>.
    ///
    /// **Default value:** `None`
    pub fn set_size(&mut self, size: Option<u16>) -> &mut Self {
        self.size = match size {
            Some(s) => Some(s.max(1).min(2048)),
            None => None,
        };
        self
    }

    /// Sets the default image to use if the user does not have a Gravatar. If `None` is provided,
    /// then Gravatar returns a blue Gravatar logo. The default image can be either a URL or one of
    /// Gravatar's premade defaults.
    ///
    /// For more information, see <https://en.gravatar.com/site/implement/images/#default-image>.
    ///
    /// **Default value:** `None`
    pub fn set_default(&mut self, default: Option<Default>) -> &mut Self {
        self.default = default;
        self
    }

    /// If `force_default` is set to `true`, then Gravatar will always return the specified default
    /// image, whether or not the user's email exists.
    ///
    /// For more information, see <https://en.gravatar.com/site/implement/images/#force-default>.
    ///
    /// **Default value:** `false`
    pub fn set_force_default(&mut self, force_default: bool) -> &mut Self {
        self.force_default = force_default;
        self
    }

    /// Sets the maximum rating level for which Gravatar will show the user's image. If `None` is
    /// provided, then Gravatar will only deliver "G"-rated images by default. If an image is at a
    /// higher rating level than the requested one, the default image is returned instead.
    ///
    /// For more information, see <https://en.gravatar.com/site/implement/images/#rating>.
    ///
    /// **Default value:** `None`
    pub fn set_rating(&mut self, rating: Option<Rating>) -> &mut Self {
        self.rating = rating;
        self
    }

    /// If `ssl` is set to `true`, Gravatar's secure URL (https://secure.gravatar.com/avatar/...)
    /// is used. Otherwise, the non-SSL website is used instead
    /// (http://www.gravatar.com/avatar/...).
    ///
    /// **Default value:** `true`
    pub fn set_ssl(&mut self, ssl: bool) -> &mut Self {
        self.ssl = ssl;
        self
    }

    /// Returns the image URL of the user's Gravatar with all specified parameters.
    pub fn image_url(self: &Self) -> Url {
        // Generate MD5 hash of email
        let digest = Md5::new()
            .chain(&self.email.trim().to_ascii_lowercase())
            .result();
        let hash = format!("{:x}", digest);

        // Create base URL using the hash
        let mut url = Url::parse(&format!(
            "{}.gravatar.com/avatar/{}",
            if self.ssl {
                "https://secure"
            } else {
                "http://www"
            },
            hash
        ))
        .unwrap();

        if let Some(s) = self.size {
            url.query_pairs_mut().append_pair("s", &s.to_string());
        }

        if let Some(ref d) = self.default {
            let val = match d {
                Default::ImageUrl(ref u) => {
                    utf8_percent_encode(u.as_str(), DEFAULT_ENCODE_SET).to_string()
                }
                Default::Http404 => "404".to_string(),
                Default::MysteryMan => "mm".to_string(),
                Default::Identicon => "identicon".to_string(),
                Default::MonsterId => "monsterid".to_string(),
                Default::Wavatar => "wavatar".to_string(),
                Default::Retro => "retro".to_string(),
                Default::Blank => "blank".to_string(),
            };
            url.query_pairs_mut().append_pair("d", &val);
        }

        if self.force_default {
            url.query_pairs_mut().append_pair("f", "y");
        }

        if let Some(ref r) = self.rating {
            let val = match r {
                Rating::G => "g",
                Rating::Pg => "pg",
                Rating::R => "r",
                Rating::X => "x",
            };
            url.query_pairs_mut().append_pair("r", val);
        }

        url
    }
}