schleuder 2.2.4 → 3.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +138 -0
  3. data/Rakefile +136 -0
  4. data/bin/pinentry-clearpassphrase +72 -0
  5. data/bin/schleuder +9 -89
  6. data/bin/schleuder-api-daemon +4 -0
  7. data/db/migrate/20140501103532_create_lists.rb +39 -0
  8. data/db/migrate/20140501112859_create_subscriptions.rb +21 -0
  9. data/db/migrate/201508092100_add_language_to_lists.rb +11 -0
  10. data/db/migrate/20150812165700_change_keywords_admin_only_defaults.rb +8 -0
  11. data/db/migrate/20150813235800_add_forward_all_incoming_to_admins.rb +11 -0
  12. data/db/migrate/201508141727_change_send_encrypted_only_default.rb +8 -0
  13. data/db/migrate/201508222143_add_logfiles_to_keep_to_lists.rb +11 -0
  14. data/db/migrate/201508261723_rename_delivery_disabled_to_delivery_enabled_and_change_default.rb +14 -0
  15. data/db/migrate/201508261815_strip_gpg_passphrase.rb +11 -0
  16. data/db/migrate/201508261827_remove_default_mime.rb +9 -0
  17. data/db/migrate/20160501172700_fix_headers_to_meta_defaults.rb +8 -0
  18. data/db/migrate/20170713215059_add_internal_footer_to_list.rb +11 -0
  19. data/db/schema.rb +62 -0
  20. data/etc/init.d/schleuder-api-daemon +87 -0
  21. data/etc/list-defaults.yml +123 -0
  22. data/etc/postfix/schleuder_sqlite.cf +28 -0
  23. data/etc/schleuder-api-daemon.service +10 -0
  24. data/etc/schleuder.cron.weekly +6 -0
  25. data/etc/schleuder.yml +61 -0
  26. data/lib/schleuder-api-daemon.rb +420 -0
  27. data/lib/schleuder.rb +81 -47
  28. data/lib/schleuder/cli.rb +334 -0
  29. data/lib/schleuder/cli/cert.rb +24 -0
  30. data/lib/schleuder/cli/schleuder_cert_manager.rb +84 -0
  31. data/lib/schleuder/cli/subcommand_fix.rb +11 -0
  32. data/lib/schleuder/conf.rb +131 -0
  33. data/lib/schleuder/errors/active_model_error.rb +15 -0
  34. data/lib/schleuder/errors/base.rb +17 -0
  35. data/lib/schleuder/errors/decryption_failed.rb +16 -0
  36. data/lib/schleuder/errors/fatal_error.rb +13 -0
  37. data/lib/schleuder/errors/file_not_found.rb +14 -0
  38. data/lib/schleuder/errors/invalid_listname.rb +13 -0
  39. data/lib/schleuder/errors/key_adduid_failed.rb +13 -0
  40. data/lib/schleuder/errors/key_generation_failed.rb +16 -0
  41. data/lib/schleuder/errors/keyword_admin_only.rb +13 -0
  42. data/lib/schleuder/errors/list_exists.rb +13 -0
  43. data/lib/schleuder/errors/list_not_found.rb +14 -0
  44. data/lib/schleuder/errors/list_property_missing.rb +14 -0
  45. data/lib/schleuder/errors/listdir_problem.rb +16 -0
  46. data/lib/schleuder/errors/loading_list_settings_failed.rb +14 -0
  47. data/lib/schleuder/errors/message_empty.rb +14 -0
  48. data/lib/schleuder/errors/message_not_from_admin.rb +13 -0
  49. data/lib/schleuder/errors/message_sender_not_subscribed.rb +13 -0
  50. data/lib/schleuder/errors/message_too_big.rb +14 -0
  51. data/lib/schleuder/errors/message_unauthenticated.rb +13 -0
  52. data/lib/schleuder/errors/message_unencrypted.rb +13 -0
  53. data/lib/schleuder/errors/message_unsigned.rb +13 -0
  54. data/lib/schleuder/errors/standard_error.rb +5 -0
  55. data/lib/schleuder/errors/too_many_keys.rb +17 -0
  56. data/lib/schleuder/errors/unknown_list_option.rb +14 -0
  57. data/lib/schleuder/filters/auth_filter.rb +39 -0
  58. data/lib/schleuder/filters/bounces_filter.rb +12 -0
  59. data/lib/schleuder/filters/forward_filter.rb +17 -0
  60. data/lib/schleuder/filters/forward_incoming.rb +13 -0
  61. data/lib/schleuder/filters/hotmail_message_filter.rb +25 -0
  62. data/lib/schleuder/filters/max_message_size.rb +14 -0
  63. data/lib/schleuder/filters/request_filter.rb +26 -0
  64. data/lib/schleuder/filters/send_key_filter.rb +20 -0
  65. data/lib/schleuder/filters/strip_alternative_filter.rb +21 -0
  66. data/lib/schleuder/filters_runner.rb +83 -0
  67. data/lib/schleuder/gpgme/ctx.rb +274 -0
  68. data/lib/schleuder/gpgme/import_status.rb +27 -0
  69. data/lib/schleuder/gpgme/key.rb +212 -0
  70. data/lib/schleuder/gpgme/sub_key.rb +13 -0
  71. data/lib/schleuder/gpgme/user_id.rb +22 -0
  72. data/lib/schleuder/list.rb +318 -127
  73. data/lib/schleuder/list_builder.rb +139 -0
  74. data/lib/schleuder/listlogger.rb +31 -0
  75. data/lib/schleuder/logger.rb +23 -0
  76. data/lib/schleuder/logger_notifications.rb +69 -0
  77. data/lib/schleuder/mail/message.rb +482 -0
  78. data/lib/schleuder/mail/parts_list.rb +9 -0
  79. data/lib/schleuder/plugin_runners/base.rb +91 -0
  80. data/lib/schleuder/plugin_runners/list_plugins_runner.rb +24 -0
  81. data/lib/schleuder/plugin_runners/request_plugins_runner.rb +27 -0
  82. data/lib/schleuder/plugins/attach_listkey.rb +17 -0
  83. data/lib/schleuder/plugins/get_version.rb +7 -0
  84. data/lib/schleuder/plugins/key_management.rb +113 -0
  85. data/lib/schleuder/plugins/list_management.rb +15 -0
  86. data/lib/schleuder/plugins/resend.rb +196 -0
  87. data/lib/schleuder/plugins/sign_this.rb +46 -0
  88. data/lib/schleuder/plugins/subscription_management.rb +140 -0
  89. data/lib/schleuder/runner.rb +130 -0
  90. data/lib/schleuder/subscription.rb +98 -0
  91. data/lib/schleuder/validators/boolean_validator.rb +7 -0
  92. data/lib/schleuder/validators/email_validator.rb +7 -0
  93. data/lib/schleuder/validators/fingerprint_validator.rb +7 -0
  94. data/lib/schleuder/validators/greater_than_zero_validator.rb +7 -0
  95. data/lib/schleuder/validators/no_line_breaks_validator.rb +7 -0
  96. data/lib/schleuder/version.rb +1 -1
  97. data/locales/de.yml +179 -0
  98. data/locales/en.yml +179 -0
  99. metadata +305 -108
  100. checksums.yaml.gz.sig +0 -3
  101. data.tar.gz.sig +0 -2
  102. data/LICENSE +0 -339
  103. data/README +0 -32
  104. data/bin/schleuder-fix-gem-dependencies +0 -37
  105. data/bin/schleuder-init-setup +0 -37
  106. data/bin/schleuder-migrate-v2.1-to-v2.2 +0 -225
  107. data/bin/schleuder-newlist +0 -413
  108. data/contrib/check-expired-keys.rb +0 -60
  109. data/contrib/mutt-schleuder-colors.rc +0 -10
  110. data/contrib/mutt-schleuder-resend.vim +0 -24
  111. data/contrib/smtpserver.rb +0 -76
  112. data/ext/default-list.conf +0 -149
  113. data/ext/default-members.conf +0 -7
  114. data/ext/list.conf.example +0 -14
  115. data/ext/schleuder.conf +0 -64
  116. data/lib/schleuder/archiver.rb +0 -46
  117. data/lib/schleuder/crypt.rb +0 -210
  118. data/lib/schleuder/errors.rb +0 -5
  119. data/lib/schleuder/list_config.rb +0 -146
  120. data/lib/schleuder/log/listlogger.rb +0 -57
  121. data/lib/schleuder/log/outputter/emailoutputter.rb +0 -120
  122. data/lib/schleuder/log/outputter/metaemailoutputter.rb +0 -50
  123. data/lib/schleuder/log/schleuderlogger.rb +0 -34
  124. data/lib/schleuder/mail.rb +0 -873
  125. data/lib/schleuder/mailer.rb +0 -26
  126. data/lib/schleuder/member.rb +0 -69
  127. data/lib/schleuder/plugin.rb +0 -54
  128. data/lib/schleuder/processor.rb +0 -363
  129. data/lib/schleuder/schleuder_config.rb +0 -75
  130. data/lib/schleuder/storage.rb +0 -84
  131. data/lib/schleuder/utils.rb +0 -80
  132. data/man/schleuder-newlist.8 +0 -174
  133. data/man/schleuder.8 +0 -416
  134. data/plugins/README +0 -20
  135. data/plugins/manage_keys_plugin.rb +0 -113
  136. data/plugins/manage_members_plugin.rb +0 -156
  137. data/plugins/manage_self_plugin.rb +0 -26
  138. data/plugins/resend_plugin.rb +0 -35
  139. data/plugins/sign_this_plugin.rb +0 -14
  140. data/plugins/version_plugin.rb +0 -12
  141. metadata.gz.sig +0 -0
@@ -0,0 +1,46 @@
1
+ module Schleuder
2
+ module RequestPlugins
3
+ def self.sign_this(arguments, list, mail)
4
+ if mail.has_attachments?
5
+ list.logger.debug "Signing each attachment's body"
6
+ intro = I18n.t('plugins.signatures_attached')
7
+ parts = mail.attachments.map do |attachment|
8
+ make_signature_part(attachment, list)
9
+ end
10
+ [intro, parts].flatten
11
+ else
12
+ list.logger.debug "Clear-signing first available text/plain part"
13
+ clearsign(mail.first_plaintext_part)
14
+ end
15
+ end
16
+
17
+ # helper methods
18
+ private
19
+
20
+ def self.make_signature_part(attachment, list)
21
+ material = attachment.body.to_s
22
+ return nil if material.strip.blank?
23
+ file_basename = attachment.filename.presence || Digest::SHA256.hexdigest(material)
24
+ list.logger.debug "Signing #{file_basename}"
25
+ filename = "#{file_basename}.sig"
26
+ part = Mail::Part.new
27
+ part.body = detachsign(material)
28
+ part.content_type = 'application/pgp-signature'
29
+ part.content_disposition = "attachment; filename=#{filename}"
30
+ part.content_description = "OpenPGP signature for '#{file_basename}'"
31
+ part
32
+ end
33
+
34
+ def self.detachsign(thing)
35
+ crypto.sign(thing, mode: GPGME::SIG_MODE_DETACH).to_s
36
+ end
37
+
38
+ def self.clearsign(mail)
39
+ crypto.clearsign(mail.body.to_s).to_s
40
+ end
41
+
42
+ def self.crypto
43
+ @crypto ||= GPGME::Crypto.new(armor: true)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,140 @@
1
+ module Schleuder
2
+ module RequestPlugins
3
+ def self.subscribe(arguments, list, mail)
4
+ email = arguments.shift
5
+
6
+ if arguments.present?
7
+ # Collect all arguments that look like fingerprint-material
8
+ fingerprint = ''
9
+ while arguments.first.present? && arguments.first.match(/\A(0x)?[a-f0-9]+/i)
10
+ fingerprint << arguments.shift
11
+ end
12
+ # Use possibly remaining args as flags.
13
+ adminflag = arguments.shift
14
+ deliveryflag = arguments.shift
15
+ end
16
+
17
+ sub, _ = list.subscribe(email, fingerprint, adminflag, deliveryflag)
18
+
19
+ if sub.persisted?
20
+ I18n.t(
21
+ "plugins.subscription_management.subscribed",
22
+ email: sub.email,
23
+ fingerprint: sub.fingerprint,
24
+ admin: sub.admin,
25
+ delivery_enabled: sub.delivery_enabled
26
+ )
27
+ else
28
+ I18n.t(
29
+ "plugins.subscription_management.subscribing_failed",
30
+ email: sub.email,
31
+ errors: sub.errors.full_messages.join(".\n")
32
+ )
33
+ end
34
+ end
35
+
36
+ def self.unsubscribe(arguments, list, mail)
37
+ # If no address was given we unsubscribe the sender.
38
+ email = arguments.first.presence || mail.signer.email
39
+
40
+ # TODO: May signers have multiple UIDs? We don't match those currently.
41
+ if ! list.from_admin?(mail) && email != mail.signer.email
42
+ # Only admins may unsubscribe others.
43
+ return I18n.t(
44
+ "plugins.subscription_management.forbidden", email: email
45
+ )
46
+ end
47
+
48
+ sub = list.subscriptions.where(email: email).first
49
+
50
+ if sub.blank?
51
+ return I18n.t(
52
+ "plugins.subscription_management.is_not_subscribed", email: email
53
+ )
54
+ end
55
+
56
+ if res = sub.delete
57
+ I18n.t(
58
+ "plugins.subscription_management.unsubscribed", email: email
59
+ )
60
+ else
61
+ I18n.t(
62
+ "plugins.subscription_management.unsubscribing_failed",
63
+ email: email,
64
+ error: res.errors.to_a
65
+ )
66
+ end
67
+ end
68
+
69
+ def self.list_subscriptions(arguments, list, mail)
70
+ subs = if arguments.blank?
71
+ list.subscriptions.all.to_a
72
+ else
73
+ arguments.map do |argument|
74
+ list.subscriptions.where("email like ?", "%#{argument}%").to_a
75
+ end.flatten
76
+ end
77
+
78
+ if subs.blank?
79
+ return nil
80
+ end
81
+
82
+ out = [ I18n.t("plugins.subscription_management.list_of_subscriptions") ]
83
+
84
+ out << subs.map do |subscription|
85
+ # Fingerprints are at most 40 characters long, and lines shouldn't
86
+ # exceed 80 characters if possible.
87
+ s = subscription.email
88
+ if subscription.fingerprint.present?
89
+ s << "\t0x#{subscription.fingerprint}"
90
+ end
91
+ if ! subscription.delivery_enabled?
92
+ s << "\tDelivery disabled!"
93
+ end
94
+ s
95
+ end
96
+
97
+ out.join("\n")
98
+ end
99
+
100
+ def self.set_fingerprint(arguments, list, mail)
101
+ if arguments.first.match(/@/)
102
+ if arguments.first == mail.signer.email || list.from_admin?(mail)
103
+ email = arguments.shift
104
+ else
105
+ return I18n.t(
106
+ "plugins.subscription_management.set_fingerprint_only_self"
107
+ )
108
+ end
109
+ else
110
+ email = mail.signer.email
111
+ end
112
+
113
+ sub = list.subscriptions.where(email: email).first
114
+
115
+ if sub.blank?
116
+ return I18n.t(
117
+ "plugins.subscription_management.is_not_subscribed", email: email
118
+ )
119
+ end
120
+
121
+ sub.fingerprint = arguments.join
122
+
123
+ if sub.save
124
+ I18n.t(
125
+ "plugins.subscription_management.fingerprint_set",
126
+ email: email,
127
+ fingerprint: sub.fingerprint
128
+ )
129
+ else
130
+ I18n.t(
131
+ "plugins.subscription_management.setting_fingerprint_failed",
132
+ email: email,
133
+ fingerprint: arguments.last,
134
+ errors: sub.errors.to_a.join("\n")
135
+ )
136
+ end
137
+ end
138
+ end
139
+ end
140
+
@@ -0,0 +1,130 @@
1
+ module Schleuder
2
+ class Runner
3
+ def run(msg, recipient)
4
+ error = setup_list(recipient)
5
+ return error if error
6
+
7
+ logger.info "Parsing incoming email."
8
+ @mail = Mail.create_message_to_list(msg, recipient, list)
9
+
10
+ error = run_filters(Filters::Runner::PRE_SETUP_FILTERS)
11
+ return error if error
12
+
13
+ begin
14
+ # This decrypts, verifies, etc.
15
+ @mail = @mail.setup
16
+ rescue GPGME::Error::DecryptFailed
17
+ logger.warn "Decryption of incoming message failed."
18
+ return Errors::DecryptionFailed.new(list)
19
+ end
20
+
21
+ error = run_filters(Filters::Runner::POST_SETUP_FILTERS)
22
+ return error if error
23
+
24
+ if ! @mail.was_validly_signed?
25
+ logger.debug "Message was not validly signed, adding subject_prefix_in"
26
+ @mail.add_subject_prefix_in!
27
+ end
28
+
29
+ if ! @mail.was_encrypted?
30
+ logger.debug "Message was not encrypted, skipping plugins"
31
+ elsif @mail.was_validly_signed?
32
+ # Plugins
33
+ logger.debug "Message was encrypted and validly signed"
34
+ PluginRunners::ListPluginsRunner.run(list, @mail).compact
35
+ end
36
+
37
+ # Don't send empty messages over the list.
38
+ if @mail.empty?
39
+ logger.info "Message found empty, not sending it to list."
40
+ return Errors::MessageEmpty.new(@list)
41
+ end
42
+
43
+ logger.debug "Adding subject_prefix"
44
+ @mail.add_subject_prefix!
45
+
46
+ # Subscriptions
47
+ logger.debug "Creating clean copy of message"
48
+ copy = @mail.clean_copy(true)
49
+ list.send_to_subscriptions(copy)
50
+ nil
51
+ end
52
+
53
+ private
54
+
55
+ def list
56
+ @list
57
+ end
58
+
59
+ def run_filters(filters)
60
+ error = filters_runner.run(@mail, filters)
61
+ if error
62
+ if list.bounces_notify_admins?
63
+ text = "#{I18n.t('.bounces_notify_admins')}\n\n#{error}"
64
+ # TODO: raw_source is mostly blank?
65
+ logger.notify_admin text, @mail.original_message, I18n.t('notice')
66
+ end
67
+ return error
68
+ end
69
+ end
70
+
71
+ def filters_runner
72
+ @filters_runner ||= Filters::Runner.new(list)
73
+ end
74
+
75
+ def logger
76
+ list.present? && list.logger || Schleuder.logger
77
+ end
78
+
79
+ def log_and_return(error, reveal_error=false)
80
+ Schleuder.logger.error(error)
81
+ if reveal_error
82
+ error
83
+ else
84
+ # Return an unrevealing error, the sender and all bystanders don't need to know these details.
85
+ Errors::FatalError.new
86
+ end
87
+ end
88
+
89
+ def setup_list(recipient)
90
+ return @list if @list
91
+
92
+ logger.info "Loading list '#{recipient}'"
93
+ if ! @list = List.by_recipient(recipient)
94
+ return log_and_return(Errors::ListNotFound.new(recipient), true)
95
+ end
96
+
97
+ # Check neccessary permissions of crucial files.
98
+ if ! File.exist?(@list.listdir)
99
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_existing))
100
+ elsif ! File.directory?(@list.listdir)
101
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_a_directory))
102
+ elsif ! File.readable?(@list.listdir)
103
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_readable))
104
+ elsif ! File.writable?(@list.listdir)
105
+ return log_and_return(Errors::ListdirProblem.new(@list.listdir, :not_writable))
106
+ else
107
+ if File.exist?(@list.logfile) && ! File.writable?(@list.logfile)
108
+ return log_and_return(Errors::ListdirProblem.new(@list.logfile, :not_writable))
109
+ end
110
+ end
111
+
112
+ # Check basic sanity of list.
113
+ %w[fingerprint key secret_key admins].each do |attrib|
114
+ if @list.send(attrib).blank?
115
+ return log_and_return(Errors::ListPropertyMissing.new(@list.listdir, attrib))
116
+ end
117
+ end
118
+
119
+ # Set locale
120
+ if I18n.available_locales.include?(@list.language.to_sym)
121
+ I18n.locale = @list.language.to_sym
122
+ end
123
+
124
+ # This cannot be put in List, as Mail wouldn't know it then.
125
+ logger.debug "Setting GNUPGHOME to #{@list.listdir}"
126
+ ENV['GNUPGHOME'] = @list.listdir
127
+ nil
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,98 @@
1
+ module Schleuder
2
+ class Subscription < ActiveRecord::Base
3
+ belongs_to :list
4
+
5
+ validates :list_id, inclusion: {
6
+ in: -> (id) { List.pluck(:id) },
7
+ message: "must refer to an existing list"
8
+ }
9
+ validates :email, presence: true, email: true, uniqueness: {scope: :list_id}
10
+ validates :fingerprint, allow_blank: true, fingerprint: true
11
+ validates :delivery_enabled, :admin, boolean: true
12
+
13
+ default_scope { order(:email) }
14
+
15
+ scope :without_fingerprint, -> { where(fingerprint: [nil,'']) }
16
+
17
+ def to_s
18
+ email
19
+ end
20
+
21
+ def self.configurable_attributes
22
+ [:fingerprint, :admin, :delivery_enabled]
23
+ end
24
+
25
+ def fingerprint=(arg)
26
+ # Allow input to contain whitespace and '0x'-prefix, but don't store it
27
+ # into the DB.
28
+ value = arg.to_s.gsub(/\s*/, '').gsub(/^0x/, '').chomp
29
+ write_attribute(:fingerprint, value)
30
+ end
31
+
32
+ def key
33
+ # TODO: make key-related methods a concern, so we don't have to go
34
+ # through the list and neither re-implement the methods here.
35
+ # Prefix '0x' to force GnuPG to match only hex-values, not UIDs.
36
+ list.keys("0x#{self.fingerprint}").first
37
+ end
38
+
39
+ def send_mail(mail)
40
+ list.logger.debug "Preparing sending to #{self.inspect}"
41
+
42
+ if ! self.delivery_enabled
43
+ list.logger.info "Not sending to #{self.email}: delivery is disabled."
44
+ return false
45
+ end
46
+
47
+ mail = ensure_headers(mail)
48
+ gpg_opts = self.list.gpg_sign_options
49
+
50
+ if self.key.blank?
51
+ if self.list.send_encrypted_only?
52
+ notify_of_missed_message(:absent)
53
+ return false
54
+ else
55
+ list.logger.warn "Sending plaintext because no key is present!"
56
+ end
57
+ elsif ! self.key.usable?
58
+ if self.list.send_encrypted_only?
59
+ notify_of_missed_message(key.usability_issue)
60
+ return false
61
+ else
62
+ list.logger.warn "Sending plaintext because assigned key is #{key.usability_issue}!"
63
+ end
64
+ else
65
+ gpg_opts.merge!(encrypt: true, keys: {self.email => "0x#{self.fingerprint}"})
66
+ end
67
+
68
+ list.logger.info "Sending message to #{self.email}"
69
+ mail.gpg gpg_opts
70
+ mail.deliver
71
+ end
72
+
73
+ def ensure_headers(mail)
74
+ mail.to = self.email
75
+ mail.from = self.list.email
76
+ mail.sender = self.list.bounce_address
77
+ mail
78
+ end
79
+
80
+ def notify_of_missed_message(reason)
81
+ self.list.logger.warn "Not sending to #{self.email}: key is unusable because it is #{reason} and sending plain text not allowed"
82
+ mail = ensure_headers(Mail.new)
83
+ mail.subject = I18n.t('notice')
84
+ mail.body = I18n.t("missed_message_due_to_unusable_key", list_email: self.list.email) + I18n.t('errors.signoff')
85
+ mail.gpg self.list.gpg_sign_options
86
+ mail.deliver
87
+ end
88
+
89
+ def admin?
90
+ self.admin == true
91
+ end
92
+
93
+ def delete_key
94
+ list.delete_key(self.fingerprint)
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ class BooleanValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ if ! [true, false].include?(value)
4
+ record.errors.add(attribute, I18n.t("errors.must_be_boolean"))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class EmailValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ unless value =~ Conf::EMAIL_REGEXP
4
+ record.errors[attribute] << (options[:message] || I18n.t("errors.invalid_email"))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class FingerprintValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ unless value =~ /\A[a-f0-9]{32,}\z/i
4
+ record.errors[attribute] << (options[:message] || I18n.t("errors.invalid_fingerprint"))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class GreaterThanZeroValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ if value.to_i == 0
4
+ record.errors.add(attribute, I18n.t("errors.must_be_greater_than_zero"))
5
+ end
6
+ end
7
+ end